Unit testing is a great tool to ensure the quality and reliability of your code. A good test suite gives you confidence that your code does what it intends to do and that you will not introduce any bugs in your app when introducing a new feature or refactoring. Let us have a look at some tips and best practices to keep in mind when creating such a test suite.

High-quality test code

We put a lot of effort into the quality of our application code so that it performs well, is maintainable and is easy to read and understand. Although unit tests live in a separate target and don’t ship to the App Store we should treat it with the same amount of care as we treat our application code. As unit tests need to be updated every time you update your app, for example when adding a new feature or refactoring code, having flaky and hard to understand tests will slow down development.

Don’t repeat yourself

If you notice you are repeating yourself see if you can extract that logic in a helper function. A good example is when you repeatedly need to generate dummy model objects. Instead of creating a new set of objects in every test function you could extend the class in your test target with a function that generates them.

extension Movie {

  func sampleFavoriteMovies() -> [Movie] {
    // Return a list of movies.
  }
}

If you have multiple tests that test the same thing under different conditions you can extract those assertions into its own method. This is great as it makes your test more readable and maintainable. However, when some of your tests start to fail it won’t be clear which test caused the failure as it will fail inside of your custom assertion method. There is an easy way to solve this. By adding the file and line arguments with the corresponding #file and #line macros as default parameters to your method, XCTest will show the error from where the custom method was called. For example:

func assertClearsCacheAfterError(with statusCode: Int, file: StaticString = #file, line: UInt = #line) {
  // Implement the test
}

Have a clear structure

Just like with the code that you write for your app a good structure can make your test more readable and easier to understand. Personally, I like to use the following structure as a rule of thumb:

  • Given: The preconditions to the test (e.g User is logged in)
  • When: Perform the operation on the object your testing (e.g a call to logout on AuthenticationService)
  • Then: The expected result (e.g User is logged out)

Pay attention to naming

Just like with application code naming is very important. I always try to stick to the following guideline when naming test functions. I start with the word test as otherwise XCTest will not recognize it as a test case. Then I make sure to include the scenario and the expected result of the test.

final class AuthenticatorTests: XCTestCase {

  func testThrowsErrorWhenAuthenticatingWithInvalidPincode() {  
    ...
  }
}

The added benefit of clearly describing what a test does is that it is a great form of documentation. Reading all the test functions within your test class will make clear all the business rules and behavior of that class. Something which is not always immediately clear when looking at the class itself.

Don’t skip the teardown() method

When tests have shared state they can become flaky. For example, some tests might suddenly fail when they are run in a different order. The first step to reducing this flakiness is to get control over the inputs of the class that you are testing. You can do this with the help of mocks and dependency injection. If after that there is still any shared state left make sure to reset this in the teardown method of the XCTestCase or use a teardownblock in the test method itself. Enabling random test execution in your Xcode scheme is a good way to prevent issues with shared state and tests that depend on each other.

Shortcuts

Keyboard shortcuts help speed up your testing workflow. These are the ones I use most for testing:

  • CMD + U: Run all the tests that are active in the current scheme.
  • CMD + 6: Go to the test navigator which shows all tests in the scheme.
  • CTRL + OPT + CMD + G: Rerun the last run tests.

Conclusion

We have discussed different tips and tricks for writing high quality unit tests. Creating a good test suite takes time, but remember that every test you write will give you more confidence in the quality and performance of your code. If you want to learn more about testing I highly recommend you to watch the following presentations:

Engineering for testability - WWDC 2017
Testing tips and & tricks - WWDC 2018
What’s new in testing - WWDC 2018

What do you think? What are some tips and tricks that you apply when writing unit tests that are not in this list? Let me know! Contact me on Twitter @kairadiagne if you have any questions, comments or feedback.