Taking a look back at the current ShoppingCart implementation it quickly becomes clear that it is pretty useless in a real life scenario. E.g. as soon as the application is closed the content of the cart is lost. This is where things get a little more interesting, as this series is not meant to be about design decisions I’m simply going to present the solution without commenting them too much.


With the help of our trusted user defaults the shopping cart is being persisted:

struct ShoppingCart {
    static let userDefaultsKey = "shoppingcart.items"

    var items: [String] {
        let anyItems = UserDefaults.standard.array(forKey: Self.userDefaultsKey)
        guard let items = anyItems as? [String] else { return [] }
        return items
    }

    mutating func add(_ item: String) {
        var updatedItems = items
        updatedItems.append(item)
        UserDefaults.standard.setValue(updatedItems, forKey: Self.userDefaultsKey)
    }

    mutating func remove(_ item: String) {
        var updatedItems = items
        updatedItems.removeAll(where: { $0 == item })
        UserDefaults.standard.setValue(updatedItems, forKey: Self.userDefaultsKey)
    }
}

Looking at the upgraded version, functionality-wise things should be the same. But running the test case will result in a failure: failure At first glance the changes weren’t as dramatic but unfortunately we introduced side effects (of course we had those before but they were contained to the struct itself). Now with every action on the shopping cart the global state is changed.


To write meaningful tests each should be deterministic, meaning given the same input it should always yield the same output. Therefore XCTest offers two handy methods:


SetUp and TearDown

Both methods are part of the XCTestCase super class and can be overridden. They are part of the execution life cycle of each test:

graph LR;
    A[setUp] --> B[Test Method];
    B --> C[tearDown];

They are most commonly used to instantiate the test state and clean it up after the test has been executed.
For both setUp and tearDown there are throwing and async throwing versions available.


Let’s rewrite our test class to firstly fix the execution and secondly make it more secure for future changes.

final class ShoppingCartTests: XCTestCase {
    // 1
    var sut: ShoppingCart!

    override func setUp() {
        super.setUp()

        // 2
        sut = ShoppingCart()
    }

    override func tearDown() {
        super.tearDown()

        // 3
        sut = nil

        // 4
        UserDefaults.standard.removeObject(forKey: ShoppingCart.userDefaultsKey)
    }

    func testAddItem() {
        let shoppingItem = "Apple"
        sut.add(shoppingItem)
        XCTAssertEqual(sut.items, [shoppingItem])
    }

    func testRemoveItem() {
        sut.add("Pear")
        sut.remove("Pear")
        XCTAssertEqual(sut.items, [])
    }
}
  1. Firstly we move the ShoppingCart reference outside of the test methods to make future setup a lot easier
  2. Initialisation of the shopping cart so that it is available before the test is executed
  3. After each test the sut is removed to make sure it is not used by any others tests
  4. To guarantee that the tests will work with a clean UserDefaults instance it is cleared


With those small modifications the tests will run green once again.


Site Note: TearDown Blocks

In case that for a certain test case only specific conditions need to be created it makes sense to clean them up only after that one test. Therefore a test can register tearDown blocks. Those will be executed before the global tearDown.

flowchart LR
    A[setUp] --> B[Test Method]
    B -.-> C[tearDownBlock]
    C -.-> C
    C -.-> D[tearDown]
    B --> D

A tearDown block can be registered inside a test method:

func text_xyz() {
    // ...
    addTeardownBlock {
        // tearDown logic
    }
    // ..
}


Conclusion

Writing tests is not only about making the right assumptions, it’s about making the right assumptions under the right conditions. Setting up and tearing down state is vital to writing deterministic tests. The topic of correctly repeatable tests is going to discussed in more detail in a later chapter about flakiness.

See you next time 💙