Unit Testing and TDD Terminology

This is part 2 of this terminology series:

This method is difficult to test.

How do I mock this class? With a stub or mock?

Should I test private methods?

Testing in the iOS world has been gaining more attention for the last several years. However, it is still in the minority. There’re still many who are just getting their feet wet. If you’re also wondering about these questions, I’ll demystify these vocabularies for you, and give you an overview of what unit testing is, its process, and practice.

Unit testing means testing a component, or unit‘s external behavior. External behavior includes only the public methods that are exposed through an API using Swift’s protocols. Internal behavior such as private methods are not tested, because they exist only to be called by other public methods. If you test your public API thoroughly, all your private methods should already be covered.

This is the key to having non-fragile tests. The not-testing-private-methods part allows the implementation to change, as long as the external behavior still meets the requirements.

Unit tests are lightweight, meaning they are small and easy to read. If you’re doing it right, you’ll have a lot of small unit test methods that combine to test all possible input permutations to a method of your test subject. They run fast and provide instant feedback to the code you’re changing. So you’ll know as soon as your change breaks something. They provide you with the confidence that your app is always in a working state.

On the other hand, UI testing verifies that your app responds in an expected way to user inputs through touches and taps. This is at a higher level than unit testing. It is similar to integration testing in web applications. It goes end-to-end. The test execution begins with the user tap to fetch data over the network to data persistence.

It is long-running because it involves (and waits for) many moving parts. And it is also prone to multiple points of failure. The network may be down. The API server may be unreachable. The disk may run out of space.

With the definitions out of the way, let’s dive into the nitty gritty details of unit testing.

The sut, short for subject under test, or test subject is the star actor in the movie. You want to write unit tests to interrogate the sut, to scrutinize its every external behavior.

You group all the unit tests for a sut in a test suite, or test class. A test class inherits from XCTestCase, like so:

A test class uses a test lifecycle to make sure every test contained within starts with the same preconditions and reset them between every test run. This test lifecycle is scoped by the setUp() and tearDown() methods. In unit testing, there is almost always a test class for every sut.

When you create a new test class in Xcode, the new file is placed in the test target, as opposed to the application target. The test target is a collection of all test files, whereas the application target is a collection of all application files. At the beginning of a test file, you want to import the classes from your application target, so that they can be used in your tests.

The @testable keyword is used to specify the classes contained in your application module are to be used for unit testing. CleanStore is the name of your application module in your Xcode project.

A test, or test case, or test method, given a set of inputs, tests one method of the sut, and verifies that the actual outputs match the expected outputs.

An example test method signature may look like:

The test prefix in the method name is important. Only methods with this prefix in a XCTestCase subclass are executed when you press ⌘U in Xcode. You can have other supporting methods called by your test methods in a test class. But they won’t be automatically invoked when you run the tests.

What does a unit test method look like? Every unit test should have the following 3 steps:

In the Given step, you prepare the state of the sut, and the inputs to the method to be tested. The inputs are passed to the method as arguments. Seed data, or test fixtures, are used to prepare these states and inputs. These test fixtures can be a database dump from the production environment, a property list, or even hard-coded in memory using arrays or dictionaries. You don’t need very complex data here. Only a minimal set of states and inputs that are representative of the use case is necessary. They are even desired because it keeps your unit tests lightweight and run fast.

If the sut interacts with other classes, these are the external dependencies to the sut. Because one dependency can lead to more dependencies, a single test method can result in calling many methods on many classes in your app. This is undesirable because it involves way too many inputs and states. It also makes your tests slow and fragile. One test failure can cause many more tests to fail.

This can be avoided by isolating these dependencies by using test doubles. A test double stands in for an external dependency by providing canned response and initial states. There are many kinds of test doubles such as dummy, stub, spy, mock, and fake. You can read about their differences in this post.

You may have heard phrases such as mock this class, or stub this class. The words mock and stub, in this case, are simply used as verbs to mean using a test double for the class. It doesn’t necessarily mean the test double is a mock or stub.

In the When step, you make the method invocation on the test subject, and also record the outputs. The outputs are captured from the function return values.

If the method to be tested is asynchronous, the results aren’t ready until some time has passed. In these instances, you can use XCTest’s asynchronous
expectations support to wait for the results. The test will be blocked until the results are ready. There is also a timeout so that your test won’t block forever.

In the Then step, you write assertions to verify that the actual outputs match the expected outputs. If they match, an assertion is true. If not, the assertion is false, and execution stops. If all assertions are true, the test passes. If any assertion is false, the test fails, and Xcode will show you the reasons for the failures.

TDD, short for Test Driven Development is a style of development practice. When using TDD, you don’t start by writing application code. Rather, you start by writing test code. The Red-Green-Refactor cycle is at the core of TDD.

  1. Red – Write a failing test
  2. Green – Write the minimum amount of code to make the test pass
  3. Refactor – Refactor both the app code and test code

The goal is to try to reach green at each step. Repeat until all assertions are true. You’ll then have already finished the feature. You repeat this process for every feature and you’ll have an app meeting your requirements.

If you want to see how unit testing and TDD in action, check out The Clean Swift Handbook + Effective Unit Testing Bundle.

I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

Leave a Comment

Your email address will not be published.