Principles of Good Unit Tests

Module 1 of 11 0%

Principles of Good Unit Tests

Finally, let’s take a moment to discuss the principles of quality, performant tests. These principles can be summarized with the acronym FIRST, which dictate that unit tests must be Fast, Isolated, Repeatable, Self-Validating and Thorough. Let’s take a look at each one of them.

Fast:

The whole purpose of unit tests is to validate our code during development. As such, we are encourage to run our unit tests as often as we can to make sure that our code continues to work as expected.

If unit tests take a long time to run, then developers would be less likely to run them often, defeating the whole purpose. If your tests are slow, it generally means that something is wrong with either the way you wrote the test or the architecture is not set up correctly. Unit Tests should not communicate with real API’s or databases.

Isolated:

Tests should run independently and should not be affected by external state. In other words, tests should succeed regardless of which order they are run. If you notice that your tests fail when the order is reversed, that means that your tests are not properly isolated as they modify the state used by other tests.

Take for example two tests that access the same UserDefaults or Keychain instance, one deleting and another fetching data. The success of the tests might depend on which one is executed first

Be sure to always use setUp() to initialize new instances before each test and tearDown() to clean up and reset all data.

Repeatable:

Tests should produce the same result every time. If you run the test 5 times, and it succeeds 3 times and fails twice, there is a problem with the test implementation.

Typically, this can happen when your SUT has some external dependencies, like an AuthenticationService that makes real API calls. Your test code might technically be correct, but since it makes real API calls, the results might be unpredictable.

Sometimes, the response might take too long or there might be networking issues. In any case, make sure that whenever you are testing an SUT you are mocking all the dependencies and not making real API calls.

Self-Validating:

Tests should be deterministic. They should produce a clear result - either passed or failed. If a test requires users to manually check the test implementation to validate that it succeeded or not, then the test is not properly written.

In XCode, this is already handled by using XCTAssert statements to define test expectations. Make sure to use assertions in every test.

Thorough:

Tests should cover all possible scenarios. If there are nine reasons while something might fail, we should write nine unit tests to cover each one, making sure to write tests for positive scenarios as well.

In our email validation example, we could have written only 2 tests - one negative and one positive, and we would achieve 100% code coverage, but that does not mean that we have tested the implementation thoroughly, since there are multiple reasons why an email might be considered invalid.