Are the unit tests we wrote for the /api/v1/episodes endpoint sufficient? How many unit tests should we write? These are some of the questions we answer in this episode. Developers often struggle with unit testing because they aren't sure when enough is enough. As a developer, you want a simple, straightforward answer. The good news is that code coverage can provide that answer.

Enabling Code Coverage in Xcode

Xcode's built-in support for code coverage helps you understand how well your test suite covers your code. Let's start by enabling code coverage. Select the Cocoacasts scheme at the top and choose Edit Scheme... from the list of options.

Enabling Code Coverage in Xcode

Select Test on the left and click the Options tab at the top. Check the checkbox of the Code Coverage section to enable code coverage. You can optionally specify for which targets to gather coverage data. The all targets option is fine.

Enabling Code Coverage in Xcode

The next time we execute the test suite, Xcode gathers coverage data. Execute the test suite by choosing Test from Xcode's Product menu at the top. The test suite should pass without issues. We have two options to inspect the coverage data Xcode gathered. Xcode creates a coverage report we can access through the Reports Navigator. Click the Reports Navigator on the left and click Coverage to inspect the coverage report.

Enabling Code Coverage in Xcode

We are only interested in the Cocoacasts application. The coverage report shows that the test suite covers about 25% of the code we wrote. We can zoom in by expanding the coverage report for the Cocoacasts application. The coverage report shows the code coverage for each source file of the Cocoacasts target. We can zoom in even more by expanding the coverage report for APIClient.swift.

Enabling Code Coverage in Xcode

Enabling Code Coverage in Xcode

The unit tests we wrote cover around 60% of the code of the APIClient class. The report shows the code coverage for each method of the APIClient class. While that is interesting and somewhat helpful, we can't use the report to find gaps in the test suite.

The good news is that the coverage report shows that the episodes() method is fully covered by unit tests. Does that mean we can stop writing unit tests for the episodes() method and move on to the other methods of the APIClient class? The answer is yes and no.

Visualizing Code Coverage in Xcode

Hover over the episodes() method and click the small arrow that appears on the right to navigate to the implementation of the episodes() method.

Visualizing Code Coverage in Xcode

Xcode automatically enables code coverage in the source editor on the right. You can toggle code coverage in the source editor by clicking the editor options in the top right, choosing Code Coverage from the list of options. Notice that some code paths are red while others are green. You may think that the green code paths are covered by the test suite and the red code paths aren't. This isn't entirely accurate.

Xcode creates a coverage report by keeping track of the code paths that are entered when the test suite is executed. The number in the gutter on the right indicates how many times the code path was entered during the execution of the test suite. It is important that you understand what that means. It means that a coverage report is an estimation.

Let's take the initializer of the APIClient class as an example. The green highlight indicates that the initializer is covered by the unit tests we wrote. This may seem odd since we haven't written a single unit test for the initializer. In the unit tests for the APIClient class, we need an APIClient instance. We invoke the initializer in the unit tests for the APIClient class and that means the code path of the initializer is entered. To Xcode, that means the initializer is covered by the unit tests we wrote.

Even though code coverage has its limitations, it is a very useful tool that I use very often to make sure the unit tests I write cover the code I intend to cover.

Finding Gaps in the Test Suite

Code coverage indicates the episodes() method is sufficiently covered by unit tests, but that isn't entirely accurate. The episodes() method invokes the request(_:) method and it is the request(_:) method that does most of the work. Let's take a look at the code coverage of the request(_:) method. We see some green code paths, but we also see plenty of red code paths. This indicates we still have quite a bit of work to do.

The catch clause of the do-catch statement isn't covered by the test suite. We take a look at that code path later. The unit tests don't cover a number of failures, such as responses with status codes that don't fall within the success range. An invalid response isn't covered either.

What does this mean for the unit tests for the episodes() method? As I mentioned earlier, code coverage indicates the episodes() method is sufficiently covered by unit tests. Writing more unit tests for the episodes() method won't make a difference for the code coverage of the episodes() method, but it can make a difference for the APIClient class.

Let me explain what I mean. It isn't possible to unit test the request(_:) method because it is private to the APIClient class. That is a limitation we cannot work around. Does that mean we can ignore the request(_:) method? The answer is once more yes and no.

Yes because we cannot write unit tests for the request(_:) method. No because we can indirectly unit test the request(_:) method. Even though it isn't possible to access a private method from within a test target, it is possible to write unit tests for an inaccessible method. Let me explain how that works.

The episodes() method delegates most of the work to the request(_:) method, which means we can use the episodes() method to indirectly unit test the request(_:) method. Code coverage helps us understand which unit tests we need to write to cover the request(_:) method as best as possible.

Don't Compromise

Another option is declaring the request(_:) method internal instead of private. That would make writing unit tests for the APIClient class much easier. Compromising an API for the sake of testability is a red flag in my book. Even though the idea may seem noble, increasing code coverage, you should never compromise an API for the sake of testability. Access control is a key feature of the language and it is important to apply it diligently.

What's Next?

In the next episode, we apply what we learned in this episode. We use code coverage to improve the coverage of the APIClient class and we use the episodes() method to indirectly unit test the request(_:) method.