HTTP in Swift, Part 5: Testing and Mocking

Part 5 in a series on building a Swift HTTP framework:

  1. HTTP in Swift, Part 1: An Intro to HTTP
  2. HTTP in Swift, Part 2: Basic Structures
  3. HTTP in Swift, Part 3: Request Bodies
  4. HTTP in Swift, Part 4: Loading Requests
  5. HTTP in Swift, Part 5: Testing and Mocking
  6. HTTP in Swift, Part 6: Chaining Loaders
  7. HTTP in Swift, Part 7: Dynamically Modifying Requests
  8. HTTP in Swift, Part 8: Request Options
  9. HTTP in Swift, Part 9: Resetting
  10. HTTP in Swift, Part 10: Cancellation
  11. HTTP in Swift, Part 11: Throttling
  12. HTTP in Swift, Part 12: Retrying
  13. HTTP in Swift, Part 13: Basic Authentication
  14. HTTP in Swift, Part 14: OAuth Setup
  15. HTTP in Swift, Part 15: OAuth
  16. HTTP in Swift, Part 16: Composite Loaders

We’ve seen how a single method can provide the basis for loading a request over the network.

However, networks are also one of the biggest points of failure when developing an app, especially when it comes to unit tests. When we write unit tests, we want tests to be repeatable: no matter how many times we execute them, we should always get the same result. If our tests involve live network connections, we can’t guarantee that. For all the reasons our actual network requests fail, so might our unit tests.

Thus, we use mock objects to mimic the network connection, but in reality provide a consistent and repeatable façade via which we can provide fake data.

Since we have abstracted our network interface to a single method, mocking it is quite simple. Here’s an HTTPLoading implementation that always returns a 200 OK response:

public class MockLoader: HTTPLoading {

    public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {

        let urlResponse = HTTPURLResponse(url: request.url!, statusCode: HTTPStatus(rawValue: 200), httpVersion: "1.1", headerFields: nil)!
        let response = HTTPResponse(request: request, response: urlResponse, body: nil)
        completion(.success(response))

    } 

}

We can provide an instance of MockLoader anywhere we need an HTTPLoading value, and any request sent to it will result in a 200 OK response, albeit with a nil body.

When we’re writing unit tests with a mocked network connection, we are not testing the network code itself. By mocking the network layer, we remove the network as a variable, which means the network is not the thing being tested: unit tests examine the variable of an experiment.

We’ll illustrate this principle using the StarWarsAPI class we stubbed out in the previous post:

public class StarWarsAPI {
    private let loader: HTTPLoading

    public init(loader: HTTPLoading = URLSession.shared) {
        self.loader = loader
    }

    public func requestPeople(completion: @escaping (...) -> Void) {
        var r = HTTPRequest()
        r.host = "swapi.dev"
        r.path = "/api/people"

        loader.load(request: r) { result in
            // TODO: interpret the result
            completion(...)
        }
    }
}

The tests for this class will validate its behavior: we want to make sure that it behaves correctly under different situations. For example, we want to make sure that the requestPeople() method behaves correctly when it gets a 200 OK response or a 404 Not Found response or a 500 Internal Server Error. We mimic those scenarios using our MockLoader. These tests will give us the confidence to evolve the implementation of StarWarsAPI without breaking existing functionality.

In order to meet these demands, our MockLoader needs to:

  1. Guarantee that the requests coming in are the ones we’re expecting in our tests
  2. Provide a custom response for each request

My personal version of MockLoader looks roughly like this:

public class MockLoader: HTTPLoading {
    // typealiases help make method signatures simpler
    public typealias HTTPHandler = (HTTPResult) -> Void
    public typealias MockHandler = (HTTPRequest, HTTPHandler) -> Void
    
    private var nextHandlers = Array<MockHandler>()
    
    public override func load(request: HTTPRequest, completion: @escaping HTTPHandler) {
        if nextHandlers.isEmpty == false {
            let next = nextHandlers.removeFirst()
            next(request, completion)
        } else {
            let error = HTTPError(code: .cannotConnect, request: request)
            completion(.failure(error))
        }
    }
    
    @discardableResult
    public func then(_ handler: @escaping MockHandler) -> Mock {
        nextHandlers.append(handler)
        return self
    }
}

This MockLoader allows me to provide individualized implementations of how to respond to successive requests. For example:

func test_sequentialExecutions() {
    let mock = MockLoader()
    for i in 0 ..< 5 {
        mock.then { request, handler in
            XCTAssert(request.path, "/\(i)")
            handler(.success(...))
        }
    }

    for i in 0 ..< 5 {
        var r = HTTPRequest()
        r.path = "/\(i)"
        mock.load(r) { result in
            XCTAssertEqual(result.response?.statusCode, .ok)
        }
    }
}

If we were to use this MockLoader while writing tests for our StarWarsAPI class, it might look something like this (I’ve left out the XCTestExpectations because they’re not directly relevant to this discussion):

class StarWarsAPITests: XCTestCase {

    let mock = MockLoader()
    lazy var api: StarWarsAPI = { StarWarsAPI(loader: mock) }()

    func test_200_OK_WithValidBody() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 200 OK with some valid JSON */))
        }
        api.requestPeople { ...
            // assert that "StarWarsAPI" correctly decoded the response
        }
    }

    func test_200_OK_WithInvalidBody() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 200 OK but some mangled JSON */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly realized the response was bad JSON
        }
    }

    func test_404() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 404 Not Found */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly produced an error
        }
    }

    func test_DroppedConnection() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.failure(/* HTTPError of some kind */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly produced an error
        }
    }

    ...
}

When we write tests like this, we treat our StarWarsAPI as a “black box”: given specific input conditions, does it always produce the expected output result?

Our HTTPLoading abstraction makes swapping out the implementation of the networking stack a simple change. All we do is pass in a MockLoader to the initializer instead of a URLSession. The key here is realizing that by making our StarWarsAPI dependent on an interface (HTTPLoading) and not a concretion (URLSession), we have enormously enhanced its utility and made it easier to use (and test) in isolation.

This reliance on behavioral definition over a specific implementation will serve us well as we implement the rest of our framework. In the next post, we’ll change HTTPLoading into a class and add a single property that will provide the foundation for just about every possible networking behavior we can imagine.


Related️️ Posts️

HTTP in Swift, Part 16: Composite Loaders
HTTP in Swift, Part 15: OAuth
HTTP in Swift, Part 14: OAuth Setup
HTTP in Swift, Part 13: Basic Authentication
HTTP in Swift, Part 12: Retrying
HTTP in Swift, Part 11: Throttling
HTTP in Swift, Part 10: Cancellation
HTTP in Swift, Part 9: Resetting
HTTP in Swift, Part 8: Request Options
HTTP in Swift, Part 7: Dynamically Modifying Requests
HTTP in Swift, Part 6: Chaining Loaders
HTTP in Swift, Part 4: Loading Requests
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 2: Basic Structures
HTTP in Swift, Part 1: An Intro to HTTP