The interface of the RootViewModel class shows that we only need to unit test the initializer and the refresh() method. I explained earlier why I don't unit test the initializer and, in the previous episodes, we unit tested the refresh() method. Does that mean that the test suite now completely covers the RootViewModel class? The answer is no. This is a common mistake developers make. In this episode, we collect code coverage data to expose the gaps in the test suite.

Exposing Gaps in the Test Suite

It's true that black-box testing means that we don't need to unit test the private interface of the RootViewModel class, but that doesn't mean that writing one unit test completely covers the RootViewModel class. Let me show you what I mean.

Code coverage data reveals how well a test suite covers the code under test. Code coverage is built into Xcode as of version 7. Click the scheme at the top and choose Edit Scheme... from the menu.

Editing the Scheme

Select Test on the left and click the Options tab at the top. Xcode can collect and visualize coverage data for you. Click the checkbox with label Code Coverage and choose all targets from the dropdown on the right.

Editing the Scheme

Click the Close button and run the test suite by choosing Test from the Product menu. The unit tests should still pass. With code coverage enabled, Xcode inspects which code paths are hit by the test suite.

We can visualize the code coverage data in the code editor. Choose Editor > Show Code Coverage from Xcode's menu and open RootViewModel.swift. Xcode displays code coverage annotations on the right of the code editor. The number indicates how many times the code path was hit by the test suite. Code paths that were not hit by the test suite are highlighted in red.

Does this mean that everything should be green? The answer is no. Code coverage isn't a silver bullet and I primarily use it to help me find gaps in the test suite. It's important that you understand that code coverage isn't perfect. Xcode's code coverage monitors the code paths that are hit by the test suite, but that approach isn't flawless. Take a look at the AppDelegate class as an example.

The application(_:didFinishLaunchingWithOptions:) method of the AppDelegate class is highlighted in green with the exception of the else clause of the guard statement. You may be wondering how that's possible. We haven't written a single unit test for the AppDelegate class.

Unit tests run in the context of your application and the application's process hosts the execution of the unit tests. The test suite starts its execution after receiving the didFinishLaunchingNotification notification. Every time the test suite is run the application(_:didFinishLaunchingWithOptions:) method of the AppDelegate class is executed and that is why it is highlighted in green. You could say it's a false positive.

This isn't a problem as long as you're aware of the limitations of code coverage. Use it as a tool, not as the absolute truth.

What's Next?

With code coverage enabled, we can inspect the gaps in the unit tests for the RootViewModel class. Open RootViewModel.swift and take a look at the code coverage annotations on the right.

Xcode shows us that we're only unit testing the happy path. What happens if the application fails to fetch the location of the user's device? What happens if the application isn't able to fetch weather data? In the next episode, we fill in the gaps in the test suite.