Lately I’ve found myself writing asynchronous network calls on mobile platforms in the reactive style. I want to share how you can test-drive calls like this for iOS. The following example uses SwiftyJSON for JSON deseriaization and FutureKit as the asynchronous framework surrounding the network call. There are several libraries in iOS that do both of these things, but your structure here will look similar with any of them.
This example is updated to Swift 3.
0. Overall Structure
On the implementation side, you’ll have two components. First, a service will wrap the network call and return a future. Second, some object will call that service on the main thread and respond to the way the result completes (in our case, this will be a
ViewController). So this means we’ll need to mock the result of the network call that the service makes. We’ll also need to test each of the different ways the future can complete to make sure the
ViewController responds the way we would like.
Now let’s look at some code from a sample app about…villains.
1. Unit Testing the Service
We’ll be unit testing the service here with the standard test runner for iOS, XCTest (though you could also use Quick if you prefer), and with matchers from Nimble (although Hamcrest or XCTassert would do fine as well).
As mentioned above, this service wraps the network call itself, which in iOS means calling
dataTask() on your session object, specified as
URLSession.shared. We need to mock that object to ensure that our test asserts on the functionality of our code and not the iOS framework. We also want to stub the response from this call, which is a
URLSessionDataTask on which we call
On line 42, we call the method we’re testing. On line 44, we trigger the response. So
session.dataTask() takes a completion handler closure that accepts data, a response, and an error. When we call that method on our mock, our mock will save off the completion handler that the service passes in. Then we’ll call that completion handler with the data that we want the service to think that the network call returned.
This is what your mocks look like so you can perform those assertions on your session and on the task that your
session.dataTask() call returns:
2. Implementing the Service
So what does this service look like? It keeps the session as a stored field and calls
session.dataTask(). The key piece that differentiates our service from that method, and our service from other services, is the completion handler that we’re passing into that method. That completion handler will know what kind of responses to expect. Our service will make a new Promise of the type of response it expects to receive, and it will parse the response we get to determine how to complete the promise. Then we’ll return a future of that promise, which our caller will use to respond when the promise completes.
3. Unit Testing the ViewController
So we have our service that makes a call to get a list of villains. Suppose we want to show that list of villains on a screen when it comes back. So we’re going to make a VillainsMasterViewController to see the villains. But first, let’s look at the structure of our unit tests for the ViewController’s responses when our promise completes.
You’ll see in this example test some of the other important concepts we have discussed in the past around testing iOS apps, namely mocking dependencies and presenting ViewControllers in tests. If you are interested or you need a refresher, check out these posts:
On to the code:
4. Implementing the ViewController Response
This part of the asynchronous call structure feels really nice. We make the call to our VillainService in our ViewController. The service returns a future, and we chain our response blocks onto that future in the ViewController so they will be triggered when the promise completes.
Testing your asynchronous network calls in iOS requires a solid understanding of how to isolate the objects under test; once that is done, you can manipulate the network responses and call completions with as little as a line of code to test your implementation. These tests run independently of your actual network call to give you an idea of the state of the mobile code in isolation. Most importantly, tests like this can help you identify when you have mishandled a case while adding code or refactoring.