Home Do Not Create Your SUT in the setUpWithError
Post
Cancel

Do Not Create Your SUT in the setUpWithError

As a beginner in unit testing, you may face confusion when setting up your system under test (SUT). If you’re not familiar with the workings of the XCTest Framework, you may encounter unexpected errors. Additionally, different test scenarios may require different SUT configurations, making it unclear how to set up your SUT in the most efficient way. In this article, I’ll delve into the lifecycle of test cases and provide a better method for creating a SUT instead of initializing it in the setUpWithError method.

A XCTestCase’s Lifecycle

A common mistake is like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import XCTest

class SomeClass {
    var val = 0
}

class SomeClassTests: XCTestCase {

    var sut = SomeClass()

    func test_sut_addOne() {
        sut.val += 1
        XCTAssertEqual(sut.val, 1)
    }

    func test_sut_addAnotherOne() {
        sut.val += 1
        XCTAssertEqual(sut.val, 2) 
        // test_sut_addAnotherOne(): XCTAssertEqual failed: ("1") is not equal to ("2")
    }

}

We create the SUT in the SomeClassTests scope and assume its property val will persist across multiple test cases. However, the failing test message shows it’s not, as the val in test_sut_addAnotherOne doesn’t equal 2. This happens because the XCTestFramework creates a unique instance for each test case, meaning that the SUT is independent between test cases and does not persist from one case to another.

One solution to this problem might be to initialize the SUT inside each test case, but this approach can quickly become cumbersome. To avoid this, many developers create the SUT inside the setUpWithError method and set it to nil in the tearDownWithError method (the old names before Xcode 11.4 were setUp and tearDown). The setUpWithError method is invoked before each test case, and the tearDownWithError method is called after each test case has finished. Here is an example of this implementation:

1
2
3
4
5
6
7
8
9
var sut: SomeClass!

override func setUpWithError() throws {
    sut = SomeClass()
}

override func tearDownWithError() throws {
    sut = nil
}

The drawback of this centralized initialization approach is that it lacks flexibility in configuring the SUT. As previously mentioned, we need to test various scenarios that may require different setups. This method makes it challenging to modify the SUT. While property injection can be used to achieve this, it would add a lot of boilerplate code to each test case. On top of that, you would need to scroll up or down to check the SUT setup, making it harder to understand the context and read the code.

Create Your SUT via A Factory Method

An ideal solution to this problem is to use a factory method. By using constructor injection, you can tailor your SUT to match the specific requirements of each test case. For example:

1
2
3
4
5
6
7
8
class ValidatorTests: XCTestCase {

    //...

    private func makeSUT(account: String? = nil, password: String? = nil) -> Validator {
        return Validator(account: account, password: password)
    }
}

You can call this helper function to create the SUT you need:

1
2
3
4
5
6
7
8
9
func test_empty_account {
    let sut = makeSUT(password: "any pwd")
    // ...
}

func test_empty_password {
    let sut = makeSUT(account: "any account")
    // ...
}

In this way, we easily resolve the configuration issue. By creating the SUT within each test case, as it lives in the test case scope, also improves readability. But it doesn’t mean the setUpWithError function is useless. In some cases, when global state needs to be set up and torn down, such as a singleton or database, using the setUpWithError and tearDownWithError may be a suitable choice.

This post is licensed under CC BY 4.0 by the author.

Delayed Deallocation Caused By Nested Closures

Data Sync Problem After Support Cloud Kit for Core Data

Comments powered by Disqus.