Chapter 7. Code Coverage and Continuous Integration

We now have about 80 tests and the code that makes tests pass, but do the tests really test all the code? Using TDD, the code coverage of our tests should be quite high. Should.

Instead of guessing, we would rather have numbers that tell us how good the code coverage of our tests really is. Before Xcode 7, it was quite difficult to measure the coverage of a test suite, but with version 7, Apple added this feature to Xcode.

In this chapter, we will measure the code coverage of our tests, and we will take a look at how we can use Xcode Server and fastlane to automate everyday tasks in our lives as iOS developers. The chapter is structured like this:

Measuring the code coverage of our tests gives us a feeling of completeness about our test suite. While following the TDD workflow, as we don't write any code without a failing test, the code coverage of our project should be very high. We don't expect it to be 100%, meaning that all the code paths are executed in the tests because the static analyzer forces us to write code that we don't expect to be executed. For example, in the code we wrote, we often used guard to make sure that the value we wanted to access was not nil. We could have written tests for a case where the value was nil. But in my opinion, in most cases these tests give no additional value.

Nevertheless, we will examine the parts of the project without code coverage and discuss whether we need to add tests to cover them.

Xcode has added native support for the measurement of the code coverage of tests with version 7. To enable it, select Edit Scheme... in the Scheme selector in Xcode:

Code coverage in Xcode

In the following pop-up window, select the Test phase and check Gather coverage data:

Code coverage in Xcode

That is all! If you have tried to add the gathering of code coverage in Xcode 6, you will most probably be impressed by how easy this is in Xcode 8. Close the window, and run all the tests to measure the code coverage.

After the tests have finished, select the Report Navigator, click on Test, and select the Coverage tab, as shown in the following screenshot:

Code coverage in Xcode

This opens the Coverage data view. On the left-hand side, you can see the files in the project, and on the right-hand side, the corresponding coverage value is shown. The worst coverage is in AppDelegate.swift. Click on the triangle next to the filename to expand its details. The details show the coverage data for all the methods in the file. It immediately becomes clear why the code coverage in AppDelegate.swift is that low. We left the methods from the template in AppDelegate even though we don't need them.

Let's remove the unused methods. Open AppDelegate.swift. The only really required method here is application(_:didFinishLaunchingWithOptions:). Remove all the other methods, run all the tests, and open the code coverage for the new test run. Now AppDelegate has 100% test coverage. Great!

Let's take a look at another file where the code coverage is not 100%. Open DetailViewController.swift and go to Editor | Show Code Coverage. If you cannot find the menu item and, instead, there is an item called Hide Code Coverage, it means that your editor is already set up correctly. With this setting, Xcode shows the coverage data in the editor next to the code:

Code coverage in Xcode

The numbers show how often this code block has been executed during the test's run. If the number is 0, it means that this line did not get executed. In the case of DetailViewController.swift, the following line has no code coverage:

guard let itemInfo = itemInfo else { return } 

To take a look at what is going on here, let's replace this line with the following equivalent implementation:

guard let itemInfo = itemInfo 
  else { return } 

Run the tests again to collect the coverage data. The code coverage is zero in the line with the else clause. The reason for this is that we did not write a test for the case when itemInfo was nil. Do we need this test? In my opinion, in this case, it does not make sense to add a test for this because we will just return from viewWillAppear(_:) when itemInfo is nil. In addition to this, in our app, the only controller that creates an instance of DetailViewController is ItemListViewController, and we already have a test that this controller sets in the itemInfo dictionary.

In fact, it is a development error if we forget to set itemInfo because then the detail View Controller will not be able to show any useful data. So, instead of adding a test, we'd rather make sure that the app crashes when there isn't itemInfo at this point. Then, such an error would show up as a crash during development. We also find the error faster than in a case where we just return from viewWillAppear(_:), and wonder why the UI is not populated with data.

To make the app crash in case there is no itemInfo, replace the guard statement with the following:

guard let itemInfo = itemInfo 
  else { fatalError() }