Unit tests best practices in Xcode and Swift

Writing unit tests is just as important as writing your application code. Unit tests are oftentimes the first to be skipped when a deadline is coming close although exactly this might slow down the project in the end. You’ll thank your future self once your project has grown and you’ve written a lot of unit tests to cover most of your apps logic.

This will not cover the testing of memory leaks or writing UI tests for share extensions but will mainly focus on writing better unit tests. I’ll also share my best practices which helped me to develop better and more stable apps.

Once you’ve written your tests it’s time to run them. With the following tips, this has become just a bit more productive.

What is unit testing?

Unit tests are automated tests that run and validate a piece of code (known as the “unit”) to make sure it behaves as intended and meets its design.

Unit tests have their own target in Xcode and are written using the XCTest framework. A subclass of XCTestCase contains test methods to run in which only the methods starting with “test” will be parsed by Xcode and available to run.

/// A simple struct containing a list of users.
struct UsersViewModel {
    let users: [String]

    var hasUsers: Bool {
        return !users.isEmpty
    }
}

/// A test case to validate our logic inside the `UsersViewModel`.
final class UsersViewModelTests: XCTestCase {

    /// It should correctly reflect whether it has users.
    func testHasUsers() {
        let viewModel = UsersViewModel(users: ["Antoine", "Jaap", "Lady"])
        XCTAssertTrue(viewModel.hasUsers)
    }
}

Your mindset when writing unit tests

Your mindset is a great starting point for writing quality unit tests. With a few basic principles, you make sure to be productive, retain focus, and write the tests your application needs te most.

Your test code is just as important as your application code

Before we dive into actual practical tips I first want to mention an important mindset. Just like when writing your application code you should try and do your best to write quality code for tests.

Think about reusing code, using protocols, defining properties if they’re used in multiple tests and make sure your tests clean up any created data. This will make your unit tests easier to maintain and prevents flaky and weirdly failing tests.

100% code coverage should not be your target

Although it’s a target for plenty, 100% coverage should not be your main goal when writing tests. Make sure to test at least your most important business logic at first as this is already a great start. Reaching 100% can be quite time consuming while the benefits are not always that big. In fact, it might take a lot of effort to even reach 100%.

On top of that, 100% coverage can be quite misleading. The above unit test example has 100% coverage as it reached all methods. However, it did not test all scenarios as it only tested with a non-empty array while there could also be a scenario with an empty array in which the hasUsers property should return false.

Unit tests code coverage
Unit tests code coverage can be enabled by editing your scheme

Write a test before fixing a bug

It’s tempting to jump onto a bug and fix it as soon as possible. Although this is great, it would be even better if you could prevent the same bug from recurring in the future.

By writing a unit test before fixing the bug you make sure that the same bug is not happening ever again. See it as “Test-Driven Bug Fixing”, also known as TDBF from now on ;-).

Writing unit tests in Swift

Now that you have the right mindset it’s time to actually go over a few tips for writing unit tests in Swift. There are multiple ways to test the same outcome while it does not always give the same feedback when a test fails. The following tips help you write tests that help you as a developer.

Naming test cases and methods

Giving both your test cases and methods a good name helps you to quickly identify a failing test. Also, it helps you to explore whether you’ve already tested a certain scenario or piece of code.

To easily find a test case for a certain class it’s recommended to use the same naming combined with “Tests”. Just like in the above example in which we named UsersViewModelTests based on the UsersViewModel naming.

Do not use XCTAssert for everything

The following lines of code all test exactly the same outcome:

func testEmptyListOfUsers() {
    let viewModel = UsersViewModel(users: ["Ed", "Edd", "Eddy"])
    XCTAssert(viewModel.users.count == 0)
    XCTAssertTrue(viewModel.users.count == 0)
    XCTAssertEqual(viewModel.users.count, 0)
}

As you can see, the method is using a describing name that tells to test an empty list of users. However, our defined view model is not empty and therefore, all assertions fail.

Using the right assertions in unit tests
Using the right assertions in unit tests

The results are showing why it’s important to use the right assertion for the type of validation. The XCTAssertEqual method is giving us way more context on why the assertion failed. This is not only showing in the red errors but also in the console logs which helps you identify failing tests a lot faster.

Setup and Teardown

Parameters used in multiple test methods can be defined as properties in your test case class. You can use the setUp() method to set up the initial state for each test method and the tearDown() method to clean up.

Apple has some great documentation on these methods that will help you understand how to use them in the right way.

Tip: You can also add assertions to the setup and tear down methods to make them run for every test.

Throwing methods

Just like when writing application code you can also define a throwing test method. This allows you to make a test fail whenever a method inside the test is throwing an error.

func testDecoding() throws {
    /// When the Data initializer is throwing an error, the test will fail.
    let jsonData = try Data(contentsOf: URL(string: "user.json")!)

    /// The `XCTAssertNoThrow` can be used to get extra context about the throw
    XCTAssertNoThrow(try JSONDecoder().decode(User.self, from: jsonData))
}

The XCTAssertNoThrow method can be used when the result of the throwing method is not needed in any further execution of the test.

Make sure to check out the opposite XCTAssertThrowsError method for matching the expecting error type.

Unwrapping optional values

New in Xcode 11 is the XCTUnwrap method that is best used in a throwing test method as it’s a throwing assertion.

func testFirstNameNotEmpty() throws {
    let viewModel = UsersViewModel(users: ["Antoine", "Maaike", "Jaap"])

    let firstName =  try XCTUnwrap(viewModel.users.first)
    XCTAssertFalse(firstName.isEmpty)
}

XCTUnwrap asserts that an Optional variable’s value is not nil and returns its value if the assertion succeeds. It prevents you from writing a XCTAssertNotNil combined with unwrapping or dealing with conditional chaining for the rest of the test code.

Running unit tests in Xcode

Once you’ve written your tests it’s time to run them. With the following tips, this has become just a bit more productive.

Re-run latest test

Re-run your last run test again by using:
⌃ Control + ⌥ Option + ⌘ Command + G.

Run a combination of tests

Select the tests you want to run by using CTRL or SHIFT, right-click and select “Run X Test Methods”.

Run multiple unit tests at once
Run multiple unit tests at once

Applying filters in the test navigator

The filter bar at the bottom of the test navigator allows you to narrow your tests overview.

The test navigator filter bar
The test navigator filter bar
  • Use the search field to search for a specific test based on its name
  • Show only tests for the currently selected scheme. This is useful if you have multiple test schemes.
  • Only show the failed tests. This will help you to quickly find which test has failed.

Enable coverage in the sidebar

Show the test coverage iteration count
Show the test coverage iteration count

Showing the test iteration count shows you whether a certain piece of code is hit during the last run test.

Test coverage is shown inline
Test coverage is shown inline

It shows the number of iterations (18 in the above example) and a piece of code turns green when it has been reached. When a piece of code is red it means that it was not covered during the last run tests.

Conclusion

As you can see there is a lot you can do to make writing unit tests a lot easier. Change your mindset, use the right setup and do not use XCTAssert everywhere.

Thanks!