The Cowardly Test-Phobe's Guide to iOS Testing: Networks

May 29, 2014 00:00 · 1075 words · 6 minute read

(This is the second part of a text-based version of the talk I gave for iOS Con at Skillsmatter in London on Friday 16th May. If you prefer the full multimedia experience, there’s video available behind a login wall at https://skillsmatter.com/skillscasts/5167-tdd-in-ios.)

The presentation slides are available here.

Testing networks

Unless you’re in the business of writing fairly trivial apps, eventually your code is going to need to talk to some external services reachable across a network link. That immediately opens up a whole world of problems that you need to deal with. Availability, latency, and quality of service are the issues that your app is going to have to handle, while you’re also going to need to make decisions about how to inform your user of what is going on.

It’s very easy to fall into the trap of building apps that work beautifully in the Simulator when sat on a Gigabit ethernet segment downstream from a multi-megabit fibre broadband connection. But the real world isn’t like that - your app needs to be able to handle the flakiest of ropey Edge services, not just full-fat wifi. Forgetting to handle those edge cases is a quick way to build something with really sucky user experiences.

It (should) go without saying that this needs to be tested. But that can be tricky - very often the APIs that your app is talking to aren’t under your control. They’re designed to be reliable and return valid data - so how can you test for the edge cases?

There are a couple of solutions, both of which rely on creating “stunt double” APIs that can stand in for the real thing. By tweaking your mock API, you can develop and test both happy paths and edge cases without any dependencies on live services.

Mocking APIs with servers

If you’re dealing with a relatively trivial API, the simplest option may be to whip up a standalone server and point your app at that for testing purposes. If you know Sinatra or Node, creating a mock API that accepts calls from your app and returns the contents of some predefined data files stored locally to the server isn’t that difficult.

But that a) presupposes that you do have those kind of technologies at your disposal, and b) creates another set of dependencies. In order to run the tests, you’ll need to make sure that your server is up and running, and returning the right values for a given endpoint. What would be far more elegant is a situation where all the moving parts needed for testing could somehow be bundled into your Xcode project.

Then you also need to make sure that your tests are calling the test API, while your live app talks to the production version. You don’t need too much imagination to see what could potentially go wrong here…

Mocking APIs

A practical alternative to a standalone server is a network stubbing library. That sits inside your test target and intercepts any calls to the network in order to return data that you define. One of the most widely used is OHHTTPStubs.

OHHTTPStubs works by catching calls from NSURLConnection and NSURLSession, and checking whether the request should either be passed through as normal or intercepted by the library. If the call is to be intercepted, the library handles creating and returning the data - the call doesn’t get out to the actual network.

There’s also the ability to manipulate the way the response is sent back - for example, setting a simulated delay or latency, returning custom HTTP headers or response codes, or just behaving as if all the packets were dropped. By changing responses, it becomes possible to test a variety of situations ranging from perfect network connectivity to complete isolation.

Setting up OHHTTPStubs

OHHTTPStubs is easiest to install using Cocoapods. Add pod 'OHHTTPStubs' to your Podfile, run pod install, and you’re good to go.

However, now is a good point to introduce a caveat. The library makes extensive use of private frameworks to swizzle the functionality into place, so including it in an app that you try to ship to the App Store is a Very Bad Idea Indeed. There are a couple of ways around this: one is to remember to take it out (not recommended); the other is to only include the library in your test target.

Assuming your project is called MyFantasticApp, then your Podfile should look like this:

platform :ios, "7.1"

...
main pods go here
...

target 'MyFantasticAppTests', :exclusive => true do

    pod 'OHHTTPStubs'

    ...
    other pods as required
    ...

end

As the MyFantasticAppTests target is separate from the main one, OHHTTPStubs won’t get compiled in when you build the project.

How it works

The basic syntax of using OHHTTPStubs looks like this:

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {

   < test the request to see if we want to stub it, and
     and return YES if we do >

} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {

    < Create and return an OHHTTPStubsResponse object with the
      data that we want to return >

}];

# Which request to stub?

In the first block, we’re examining the NSURLRequest to see if it’s one we want to stub out - this allows you to pass through some requests, but catch others.

If you want to stub ALL requests, you simple return YES from this block. Otherwise, you can be more subtle:

  • catching all requests to a specific host:

// Catch http://adoptioncurve.net return [request.URL.host isEqualToString:@“adoptioncurve.net”];`

  • catching all requests to a specific URL:

// catch http://adoptioncurve.net/about return [request.URL.lastPathComponent isEqualToString:@“about”];`

  • catching requests with a specific query string

// Catch http://adoptioncurve.net/login?id=1234 return [request.URL.query isEqualToString:@“id=1234”];`

# How to return data

Once the stubRequestsPassingTest: block has returned YES, you’ll need to create an OHHTTTPStubsResponse object to return to the method that made the original request. This mimics the data payload that the API would return.

There are several ways of doing this:

  • responseWithData:statusCode:headers: allows you to create an NSData object yourself, and return it along with an HTTP status code and HTTP headers

  • responseWithFileAtPath:statusCode:headers: allows the contents of a file to be returned, along with status codes and headers. This file can be JSON, HTML, binary data or whatever format your API will return - the only requirement is that it exists in the app bundle where the tests can find it.

Getting sample data

The first thing you’ll need when starting to use OHHTTPStubs is some data to return for your tests.