When writing unit tests for Swift networking code that uses URLSession we don’t want to perform real network requests. We want to mock responses so that our unit tests remain fast, predictable and independent. Mocking responses from URLSession can be done by wrapping it in a protocol and creating a mock session when testing. But an alternative way is using URLProtocol.

What is URLProtocol?

URLProtocol is a class that allows you to intercept and handle network requests, giving you the ability to implement features such as caching, redirects, or authentication. When it comes to testing, it gives the ability to intercept requests and return mocked responses, as well as spy on the request that was sent.

To achieve this we implement our own subclass of URLProtocol, register it with the URL loading system, override some of the methods that the URL loading system calls during a network request and return our own data.

Let’s implement it.

Create a URLProtocol Subclass to Mock Responses

1. Create a Subclass of URLProtocol

First off create a subclass of URLProtocol. We’ll call it TestURLProtocol.

final class TestURLProtocol: URLProtocol { }

2. Create a store for Mock Responses

Create a property on the class that will store the mock responses. When we come to load the mock responses into the URL loading system we will see that the system expects the type (URLRequest) -> (result: Result<Data, Error>, statusCode: Int?). Therefore our store of mock responses will take a URL as a key and each key will point to this type. To make it more clear we can add a typealias.

final class TestURLProtocol: URLProtocol {
    typealias MockResponse = (URLRequest) -> (result: Result<Data, Error>, statusCode: Int?)
    static var mockResponses: [URL: MockResponse] = [:]
}

This means before we run a test we can configure the mocked response based on the URL that is being requested. In the example below, we assign a successful response of valid JSON data with a status code of 200 to the Endpoint.cars.url key. This means if a test tries to fetch Endpoint.cars.url, we will be able to intercept it, get this closure from the dictionary and use it for our response instead.

TestURLProtocol.mockResponses[Endpoint.cars.url] = { request in
    return (result: .success(validJson.data(using: .utf8)!), statusCode: 200)
}

3. Override URLProtocol methods

There are a few methods we need to override in order to intercept the requests.

The first two are class functions. The first is canInit(with request: URLRequest) -> Bool, which checks whether the URL loading system can handle the request. In our case, we want to return true if the request URL matches any of the URLs in our mockResponses dictionary. The other is canonicalRequest(for request: URLRequest) -> URLRequest, for this we just return the request that was passed in as a parameter.

public override class func canInit(with request: URLRequest) -> Bool {
    guard let url = request.url else { return false }
    return mockResponses.keys.contains(url.removingQueries)
}

public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
    request
}

We also need to override func stopLoading() but it requires no implementation for our mocking.

public override func stopLoading() {}

Finally we override func startLoading(). This is where the bulk of our work happens. We use a guard block to get the mocked response from our mockResponses dictionary. This fails with fatalError if there is no response registered for the expected URL.

Then we call the response block with the request. This will return the mocked data.

We handle the optional HTTP status code in an if-let block and inform the client. The client is a URLProtocolClient. It is the interface used by URLProtocol subclasses to communicate with the URL Loading System.

Finally, we handle both the error and failure scenarios and inform the client.

In the end, it looks like this.

public override func startLoading() {
    guard let responseBlock = TestURLProtocol.mockResponses[request.url!.removingQueries] else {
        fatalError("No mock response")
    }
    
    let response = responseBlock(request)
    
    if let statusCode = response.statusCode {
        let httpURLResponse = HTTPURLResponse(url: request.url!,
                                              statusCode: statusCode,
                                              httpVersion: nil,
                                              headerFields: nil)!
        self.client?.urlProtocol(self,
                                 didReceive: httpURLResponse,
                                 cacheStoragePolicy: .notAllowed)
    }
    
    switch response.result {
    case let .success(data):
        client?.urlProtocol(self, didLoad: data)
        client?.urlProtocolDidFinishLoading(self)
        
    case let .failure(error):
        client?.urlProtocol(self, didFailWithError: error)
    }
}

4. Use TestURLProtocol

For the URL loading system to use TestURLProtocol we need to register it. As we plan to use this class in tests we can do this in our setUp and tearDown methods of XCTestCase. We can also remove all responses in the tearDown to keep a clean state for each test.

override func setUp() {
	super.setUp()
	URLProtocol.registerClass(TestURLProtocol.self)
}

override func tearDown() {
	TestURLProtocol.mockResponses.removeAll()
	URLProtocol.unregisterClass(TestURLProtocol.self)
	super.tearDown()
}

Finally, we can mock whatever data and errors we need in our tests. Here’s a mocked successful request.

let jsonData = #"{ "name": "pickachu" }"#.data(using: .utf8)!
TestURLProtocol.mockResponses[url] = { request in
    (result: .success(jsonData), statusCode: 200)
}

Here’s a mocked error.

struct TestError: Error {}
TestURLProtocol.mockResponses[url] = { request in
	(result: .failure(TestError()), statusCode: nil)
}

Conclusion

Intercepting and mocking network responses with URLProtocol subclasses is quick, easy and convenient. If you want to try it, feel free to use my code here: https://github.com/mgopsill/TestURLProtocol.