Unit Testing Network Calls in Swift: Dependency Injection

Recently, I was having issues with performing tests over the closed network we have at work. While our development Macs have access to the internet, our CI box does not (don’t ask me why).

Turns out there’s a whole bunch of reasons for why you don’t actually wantyour CI boxes running tests over a network anyway.

For starters, you shouldn’t be testing someone else’s code. Servers can go down, and APIs can change. Your backend devs should already have this covered in their own tests, and if they don’t, then you have bigger problems.

What about when you want your network calls to fail? How do you do that reliably? If your request returns mangled data, you’ll need to make sure your app can handle it elegantly, rather than just straight up crashing.

NSURLSession

The obvious place to start would be having a way for us to fake our network calls. Overriding a few functions in NSURLSession and NSURLSessionDataTask should just about do it.

This is a great little snippet that you can just copy/paste into any project. Throw it in the test bundle so you’re not shipping it with the rest of your code.

class MockSession: URLSession {
    
    var completionHandler: ((Data, URLResponse, Error) -> Void)?
    static var mockResponse: (data: Data?, URLResponse: URLResponse?, error: Error?)

    override class var shared: URLSession {
        return MockSession()
    }
    
    override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
        self.completionHandler = completionHandler
        return MockTask(response: MockSession.mockResponse, completionHandler: completionHandler)
    }
    
}
class MockTask: URLSessionDataTask {
    
    typealias Response = (data: Data?, URLResponse: URLResponse?, error: Error?)
    var mockResponse: Response
    let completionHandler: ((Data?, URLResponse?, Error?) -> Void)?
    
    init(response: Response, completionHandler: ((Data?, URLResponse?, Error?) -> Void)?) {
        self.mockResponse = response
        self.completionHandler = completionHandler
    }
    
    override func resume() {
        completionHandler!(mockResponse.data, mockResponse.URLResponse, mockResponse.error)
    }
}

Admittedly it looks a little weird, but what we’re essentially doing is overriding dataTaskWithRequest returning a fake task, where resume() is also overridden, which is what gives us back our fake response — that we will create in the next step — through the completion handler. Does that make sense?

Maybe an example of how to use it will help.

Unit Testing

So lets say we need to fetch a bunch of products, something we might want customers to buy through our app.

Products are added/removed all the time, so testing on real data would be an issue. Instead, we can create our own response and pass that into our mockResponse property on our MockSession — NSURLSession subclass. This way we have a consistent local datasource to test that our objects are being created correctly.

func testRetrieveProductsValidResponse() {
    // we have a locally stored product list in JSON format to test against.
    let testBundle = Bundle(forClass: type(of: self))
    let filepath = testBundle.pathForResource("products", ofType: "txt")
    let data = Data(contentsOfFile: filepath!)
    let urlResponse = HTTPURLResponse(url: URL(string: "https://anyurl.doesntmatter.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)
    // setup our mock response with the above data and fake response.
    MockSession.mockResponse = (data, urlResponse: urlResponse, error: nil)
    let requestsClass = RequestManager()
    // All our network calls are in here.
    requestsClass.Session = MockSession.self
    // Replace NSURLSession with our own MockSession.
    // We still need to test as if it's asynchronous. Because well, it is.
    let expectation = XCTestExpectation(description: "ready")
    // For this test, no need to pass in anything useful since it doesn't affect our mocked response.
    // This particular function fetches JSON, converts it to custom objects, and returns them.
    requestsClass.retrieveProducts("N/A", products: { (products) -> () in
        XCTAssertTrue(products.count == 7)
        expectation.fulfill()
    }) { (error) -> () in
        XCTAssertFalse(error == Errors.NetworkError, "Its a network error")
        XCTAssertFalse(error == Errors.ParseError, "Its a parsing error")
        XCTFail("Error not covered by previous asserts.")
        expectation.fulfill()
    }
    waitForExpectations(timeout: 3.0, handler: nil)
}

Thats about it!