Chapter 5. Testing Network Code

Most apps in the App Store perform networking in one way or the other. Apple provides a great class for network requests—NSURLSession. Its requests are asynchronous. This means that the response is delivered on a background thread. If that wasn't the case, the UI would freeze while the app waits for a response from the server.

The main topic of this chapter is how to test an asynchronous API. There are two ways to write tests for asynchronous API calls. Firstly, using asynchronous tests provided by the XCTest framework. Secondly, using stubs as we did in the previous chapter.

Both methods have their advantages. Asynchronous tests let us test whether the web server is implemented as described in the documentation. In addition to this, the tests are closer to the implementation of the finished app and, therefore, are more likely to find bugs that would end up in the final version.

On the flip side, stubs let us develop the network layer of our app even before the web service is implemented. We just need the documentation of the API calls and the expected responses. As the tests do not depend on communication with a server, the test execution is significantly faster.

You should have both kinds of tests in your iOS development toolbox.

This chapter covers the following topics:

In the previous chapter, we wrote a stub for CLGeocoder. Now, we will write a test that asserts that the geocoder built into CoreLocation works as we expect it to. The fetching of coordinates from a geocoder is asynchronous. This means that we have to write a test that can deal with asynchronous interfaces.

But let's first structure the files a bit in the Project Navigator of Xcode. Select all the controller files in the main target (ItemListViewController.swift, ItemListDataProvider.swift, ItemCell.swift, DetailViewController.swift, and InputViewController.swift), and control + left-click to create a group from the selection. Let's call this group Controller. Do the same with the corresponding test cases in the test target.

Now, let's get started with the test. We start naïvely. Add the following test to InputViewControllerTests:

Run the tests. All the tests pass. So, it looks like that the geocoder works as we thought it would. But wait a minute. We have skipped the red phase. In TDD, we first have to have a failing test. Otherwise, we cannot be sure whether the test actually works.

We have no access to the source of CLGeocoder, so we cannot change the implementation to make the test fail. The only thing we can do is to change the assertion. Replace the assertions within the closure with this code:

Run the tests again. Uh!? The tests still pass. To figure out what is going on, add a breakpoint in the line of the first assertion:

Implementing asynchronous tests

Run the tests again. During the execution of this test, the debugger should now stop at this line, so open the debugger console to investigate what is going on.

The debugger never reaches the breakpoint.

The reason for this is that the geocodeAddressString(_:completionHandler:) call is asynchronous. This means that the closure is called sometime in the future on a different thread, and the execution of the tests moves on. The test is finished before the callback block is executed, and the assertions never get called. We need to change the test to make it asynchronous.

Replace test_GeocoderWorksAsExpected() with the following lines of code:

The new lines are highlighted. We create an expectation using expectationWithDescription(_:). At the end of the test, we call waitForExpectationsWithTimeout(_:handler:) with a timeout of 3 seconds. This tells the test runner that it should stop at this point and wait until either all the If not all expectations are fulfilled when the timeout duration has passed, the test fails. In the callback closure, we fulfill the expectation after the assertions are called.

Now, run the tests again. The last test fails because the coordinate we get from the geocoder does not match the values we put into the 0.0 and 0.0 assertions. Replace the assertions again with the correct ones that we had when we first wrote the test:

Run the tests again. All the tests pass, and CLGeocoder works as expected.

We have just taken a look at how we can use XCTest to test asynchronous APIs. This can be used to test many different aspects of iOS development (for example, sending NSNotifications, fetching data from a web server, writing data to a database in the background, and so on). Whenever something asynchronous takes place, we can add expectations and set them as fulfilled when the asynchronous callback is executed.

This is very powerful. But keep in mind that unit tests should be fast and reliable. Therefore, it is often better to use mocks and stubs to eliminate the asynchronous nature of the API. We don't want to have failing tests only because the web server we try to talk to is down at the moment. In addition to this, we want to be able to run the tests in the plane without any Internet connection.

In the following sections, we will use stubs to eliminate the asynchrony of the APIs that we are dealing with. The additional benefit is, that we can develop the network layer of our app without a finished web server at hand. The only thing we need is a finished API documentation that is not going to change.