Testing Asynchronous Operations

How would you go about unit testing your networking calls? They are asynchronous, so the results won’t come back right away. The following is an excerpt of the latest chapter I just added to my book Effective Unit Testing on this exact topic.

All the unit tests that you’ve seen so far are for testing synchronous operations. That is, the outputs can be observed and verified immediately after invoking the method on the test subject. The outputs can be function return values, state changes, or methods invoked on a dependency. All of these happen right away in the same thread. When you write the assertions in the Then phase, you’re guaranteed that the outputs have already been set so that you can safely compare the actual v.s. expected. You don’t have to worry about whether the outputs are ready or not. They are ready.

However, a lot of stuff that we do in modern iOS development are asynchronous operations, such as Core Data, networking, or even some expensive drawing code or events. The results will come, but not right away. It takes time for the task to finish, and the outputs be set. In iOS, these asynchronous operations are usually coded in one of two ways:

  • Completion handler block
  • Delegate method

Asynchronous operations present a challenge to unit testing. In the Then phase, the results may or may not have been set to the outputs for you to observe and verify. When you write your assertions, the test may pass this time (if the outputs have been set), but fail at another time (if the outputs haven’t been set).

The most common asynchronous operation is networking. Fetching data from an API over the network has latency, because data takes time to travel through wires around the globe. There can also be numerous points of failures. The server may be down. Packets can get dropped. Multiplexing can produce errors. Connection may be lost. There are many variables that can result in errors and/or delays. As a result, asynchronous testing necessitates some special handling. Fortunately, Xcode has built-in support to help with that.

As quoted in Apple’s URL Session Programming Guide:

Like most networking APIs, the NSURLSession API is highly asynchronous. If you use the default, system-provided delegate, you must provide a completion handler block that returns data to your app when a transfer finishes successfully or with an error. Alternatively, if you provide your own custom delegate objects, the task objects call those delegates’ methods with data as it is received from the server (or, for file downloads, when the transfer is complete).

Let’s look at completion handler blocks and delegate methods, separately, in more details. We’ll use a networking example that fetches public gists from the GitHub API to illustrate how to write unit tests for asynchronous operations.

In the ListGistsWorker class of the Gister repo on GitHub, you’ll see two implementations of the fetch() method. The first one uses a completion handler block to return the result back to the caller. The second one uses a delegate method to return the result. We’ll first look at the app code, and then look at how to write unit tests for both implementations.

Completion Handler Block

The block implementation of the fetch(completionHandler:) method is as follows.

The fetch(completionHandler:) method of the GistAPI is called upon to do the actual fetching. When the gists are available, they are passed back out through the block parameter. You have already seen this million times, and it is pretty straightforward.

Since the GistAPI is an external dependency to our test subject, we will first define the GistAPISpy to conform to the GistAPIProtocol.

When the fetch(completionHandler:) method is called, we simply set fetchWithCompletionHandlerCalled to true, so we can verify the method invocation later in the Then phase.

In order to simulate the asynchronous nature of the fetch operation, we can take advantage of the asyncAfter(deadline:qos:flags:execute:) method. This method will execute the block that you pass in after a specified amount of time has passed.

We don’t want our tests to run for too long. Setting deadline to 1 second seems reasonable here. The qos and flags parameters have default arguments. We’ll just take them, so we don’t have to specify their values. In the execute block, we simply want to call the completionHandler that was passed in to the fetch(completionHandler:) method with some canned results that we can conveniently get from the seeds.

How should we test the fetch(completionHandler:) method? There are two things we want to make sure here:

  • It is delegating the fetch responsibility to the gistAPI dependency. We’ll write the testFetchShouldAskGistAPIToFetchGistsWithBlock() test to accomplish that.
  • It returns the gists results to the caller. We’ll write the testFetchShouldReturnGistsResultsToBlock() test to accomplish that.

To read the rest of the chapter, buy Effective Unit Testing now.

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. Required fields are marked *