Masilotti.com

Mocking Classes You Don't Own

My go-to approach when unit-testing Swift is protocol-oriented programming. As requested by you, let's see a real-world example. What better way to show some code than with the networking stack, something every iOS developer has dealt with!

This is part 1 in a 3-part series on Testing NSURLSession.

Don't Fear the Session!

If you are developing anything targeting iOS or tvOS 9.0 and start networking code, you will notice that the NSURLConnection APIs are officially deprecated. My go-to method, sendAsyncronousRequest(), will need to be replaced with dataTaskWithURL().

If you haven't worked with NSURLSession yet, no worries. There are only two major components you'll need to understand to follow along.

The main interface to the API, NSURLSession, can be used in a very similar manner to NSURLConnection. That is, with asynchronous blocks and no delegates. We will focus on dataTaskWithURL() from a bare bones instantiation to retrieve data from the network.

This method returns an instance of NSURLSessionDataTask. Think of these as in-flight network requests. Most importantly, they can be started, paused, cancelled, and resumed.

Here's a naive approach to sending a request to my site and printing the response.

let session = NSURLSession()
let url = NSURL(string: "http://masilotti.com")!
let task = session.dataTaskWithURL(url) { (data, _, _) -> Void in
    if let data = data {
        let string = String(data: data, encoding: NSUTF8StringEncoding)
        print(string)
    }
}
task.resume()

Not so bad, right? This method has a few limitations, but we can start with it as a building block to bigger and better queries.

After you create a task, you start it by calling its resume method. - NSURLSessionTask docs

As you read through the post, feel free to follow along with the commits on GitHub. Unfortunately XCTest is a pain to use with playgrounds so it's just an Xcode project.

Possible Testing Approaches

Our goal is to unit test the interface to NSURLSession. To do so , we will create a new object, HTTPClient, that interacts with the session. The rest of the app's code will interact with HTTPClient directly.

Possible ways to test the session:

  1. Full integration tests that hit the network
  2. Subclass NSURLSession
  3. Mock NSURLSession via a new protocol

Integration Tests That Hit the Network

Perhaps the simplest way to test the session is by letting it access the network. The request could hit a known endpoint on your server, say /ios-test. Then you can assure that the response is parsed into valid JSON or model objects. While easy to set up, this approach has a few downsides.

First, the tests will take much longer to run. If you have a poor network connection they will take even longer. This technique could easily add chunks of time to your previously blazing-fast test suite. Not to mention that the tests will fall over if you lose your internet connection!

Asynchronous tests are also not reliable. The more tests you have the higher the likelihood one or more will fail randomly. Use something like UI Testing if you want to write full integration tests.

Subclass NSURLSession

By subclassing you can easily add flags to check which methods are called with which parameters. However, if you don't override every single method, you are using an object under test that has real functionality. This means your test suite could be accessing the network or other crazy things without you being aware.

This approach becomes unscalable when dealing with Apple's framework. Every iOS release you will have to go back to all of your subclasses and update them for each new method that was added. If Apple changes the functionality under the hood your tests could also fail for unexpected reasons.

Mock NSURLSession with a Protocol

Mocking the session under test relieves us of the problems that burden the other techniques. The network will never be hit, no functionality will be executed, and we won't have to update the mock when Apple adds new methods.

IDEPEM

To accomplish this we will follow my coined acronym, IDEPEM. This stands for inject dependencies, everything's a protocol, equatable mocks.

Read through Better Unit Testing with Swift for a deeper dive into why I chose this approach.

To do this, we will need to create a few extra protocols and intermediary objects.

  1. HTTPClient needs to work with an NSURLSession
  2. NSURLSession needs to conform to a protocol so we can mock it under test
  3. We need to mock NSURLSessionDataTask so we can assert resume() is called

Make NSURLSession Testable

Ideally, the interface to NSURLSession would be protocol based. We could create a mock object that conformed to this protocol and use the objects interchangeably under test.

Unfortunately, Apple hasn't fully embraced protocol-oriented programming in its framework code. No worries; we’ll create our own protocol, and have NSURLSession conform to it via an extension.

Create the Protocol

First, let's create a protocol that Apple's NSURLSession can conform to.

typealias DataTaskResult = (NSData?, NSURLResponse?, NSError?) -> Void

protocol URLSessionProtocol {
    func dataTaskWithURL(url: NSURL, completionHandler: DataTaskResult)
      -> NSURLSessionDataTask
}

Second, give the client a session via dependency injection.

class HTTPClient {
    private let session: URLSessionProtocol

    init(session: URLSessionProtocol = NSURLSession.sharedSession()) {
        self.session = session
    }

    // ... //
}

Now when we test HTTPClient we can inject any implementation of the URLSessionProtocol we choose. In our production code we don't have to worry about creating a conforming object manually as the default parameter will instantiate an NSURLSession for us!

But wait, we seem to have an error.

nsurlsession-exception

Default argument value of type 'NSURLSession' does not conform to 'URLSessionProtocol'

Mimicking the interface to NSURLSession in our protocol isn't enough. We have to tell the compiler that it conforms to our protocol. We accomplish this with an empty protocol extension on Apple's session.

extension NSURLSession: URLSessionProtocol { }

We don't actually have to implement anything in this extension because we kept the method signature the same as Apple's framework. Meaning, NSURLSession already implements our protocols required methods.

Create a Mock for Tests

Now that we can inject any concrete implementation of URLSessionProtocol, let's create a mock to track method calls. We can use this under test to ensure the right methods were called on the session with the right parameters.

class MockURLSession: URLSessionProtocol {
    private (set) var lastURL: NSURL?

    func dataTaskWithURL(url: NSURL, completionHandler: DataTaskResult) 
      -> NSURLSessionDataTask
    {
        lastURL = url
        return NSURLSessionDataTask()
    }
}

When dataTaskWithURL() is called we take note of the URL that was passed in. We can then interrogate this later to assert the method was called with the correct parameter.

Make sure to import your Swift module in your mocks and tests with:

@testable import <MAIN_MODULE>

Test the URL

With dependency injection and our mock in place, the test almost writes itself.

class HTTPClientTests: XCTestCase {
    var subject: HTTPClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        subject = HTTPClient(session: session)
    }

    func test_GET_RequestsTheURL() {
        let url = NSURL(string: "http://masilotti.com")!

        subject.get(url) { (_, _) -> Void in }

        XCTAssert(session.lastURL === url)
    }
}

We create a mock session and inject it into the subject under test. Then we call the stimulus, get(), with a referenced URL. Finally, we assert that the URL the session received was the same one we passed in.

typealias HTTPResult = (NSData?, ErrorType?) -> Void

class HTTPClient {
    // ... //

    func get(url: NSURL, completion: HTTPResult) {
        session.dataTaskWithURL(url) { (_, _, _) -> Void in }
    }
}

This gets us pretty far in terms of testing NSURLSession. We can easily extend this approach to work with requestWithRequest() and start asserting more information on the NSURLRequest. However, we still haven't tested anything on the returned NSURLSessionDataTask, such as the call to resume().

Make NSURLSessionDataTask Testable

We can follow the same approach to start testing the data task.

  1. Create the protocol
  2. Extend the base class
  3. Create the mock
protocol URLSessionDataTaskProtocol {
    func resume()
}
extension NSURLSessionDataTask: URLSessionDataTaskProtocol { }
class MockURLSessionDataTask: URLSessionDataTaskProtocol {
    private (set) var resumeWasCalled = false

    func resume() {
        resumeWasCalled = true
    }
}

Now things get a little hairy. We need our URLSessionProtocol's method to return our protocol, not Apple's base class. Let's modify the protocol to do just that.

protocol URLSessionProtocol {
    func dataTaskWithURL(url: NSURL, completionHandler: DataTaskResult)
      -> URLSessionDataTaskProtocol
}

Uh-oh, looks like we are back to the same error as before.

nsurlsession-exception

Default argument value of type 'NSURLSession' does not conform to 'URLSessionProtocol'

Even though the error message is the same, the root cause is actually a little different.

Extend NSURLSession to Handle the New Protocol

The error is occurring because NSURLSession doesn't have a dataTaskWithURL() method that returns our custom protocol. To fix this, we just need to extend the class a little differently.

extension NSURLSession: URLSessionProtocol {
    func dataTaskWithURL(url: NSURL, completionHandler: DataTaskResult)
      -> URLSessionDataTaskProtocol 
    {
        return (dataTaskWithURL(url, completionHandler: completionHandler)
          as NSURLSessionDataTask) as URLSessionDataTaskProtocol
    }
}

Here we implement the method in the protocol manually. But instead of doing any actual work, we call back to the original implementation and cast the return value. The cast doesn't need to be implicit because our protocol already conforms to Apple's data task. Win-win!

Update the Session Mock to Return a Data Task

Now that our URLSessionProtocol returns a custom object, we need the ability to stub it out under test. We do this by adding a publicly-writable property on the mock that is returned when the method is called.

class MockURLSession: URLSessionProtocol {
    var nextDataTask = MockURLSessionDataTask()
    private (set) var lastURL: NSURL?

    func dataTaskWithURL(url: NSURL, completionHandler: DataTaskResult)
      -> URLSessionDataTaskProtocol
    {
        lastURL = url
        return nextDataTask
    }
}

The property is defaulted to something so we don't have to worry about setting it if we don't need it in our test. "It's there when you need to set it, but gets out of the way when you don't."

Test that resume() Was Called

With everything in place we can now test that the data task was started.

class HTTPClientTests: XCTestCase {
  // ... //

  func test_GET_StartsTheRequest() {
        let dataTask = MockURLSessionDataTask()
        session.nextDataTask = dataTask

        subject.get(NSURL()) { (_, _) -> Void in }

        XCTAssert(dataTask.resumeWasCalled)
    }
}

We create a reference to our mock data task and assign it to the session. After calling the stimulus, we assert that the resumeWasCalled flag was set on the data task.

class HTTPClient {
    // ... //

    func get(url: NSURL, completion: HTTPResult) {
        let task = session.dataTaskWithURL(url) { (_, _, _) -> Void in }
        task.resume()
    }
}

Pro tip: Using HTTPClient in an Xcode playground? Add the following two lines to allow the network request to finish.

import XCPlayground

XCPSetExecutionShouldContinueIndefinitely(true)

Recap

We took the IDEPEM approach to getting NSURLSession in a test harness. First, we made sure the session conformed to our custom protocol with an extension. Then we created a mock and injected it under tests. Finally, we farther extended the class to work with NSURLSessionDataTask.

But…

Seems like a lot of work to test, what, two lines of code? I agree!

Think of this post as your approach to testing NSURLSession, not an actual framework or library. You know, "teach a man to fish" and all that.

Don't believe me? Try extending this technique to test dataTaskWithRequest(). I bet you already know how to start and have a pretty good outline in your head. If you run into issues, feel free to leave a comment below and I'll help you through it.

What's Next?

Everything we've tested so far is only related to the input of the method. What happens when the network connection fails? Or returns JSON that we want to parse?

In part two we take a look at the response, DataTaskResult. We go over testing response NSData and handling network errors. And the best part? We do all of this without any asynchronous code in our tests. Read the next post on testing NSURLSession with Swift, flattening asynchronous tests.

If you liked this post, you can share it with your followers or follow me on Twitter.

More on Swift Testing