How to mock any network call with URLProtocol


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


Have you ever heard of a type called URLProtocol?

This type is not widely known and that’s a shame because it’s super useful!

It basically enables you to mock any network call, without the need to make any change to your existing code 👌

So let’s see how it works!

First let’s take a look at this code:

This is a typical example of how a network call is implemented in an iOS app.

As you would expect, the network call is made using a URLSession.

And then its result is decoded with the framework Decodable.

But in its current form, this code doesn’t provide a way to replace the actual network call by a mocked version that would return some predefined data.

And this a quite a strong limitation, because without this possibility you can’t write reliable unit tests for the class UserAPI.

However, you can notice that it’s possible to inject a custom URLSession when we initialize a UserAPI:

And as it turns out, thanks to URLProtocol, this possibility will be enough to implement a mocked network call!

So let’s move to our testing target and start actually using URLProtocol!

URLProtocol is an abstract class that we can subclass in order to implement a type that will be able to intercept network calls made through a URLSession.

And when a call is intercepted, the subclass of URLProtocol will be able to manually inject a predefined response to that call.

So let’s implement it!

First, we need to define two methods called canInit(with request:) and canonicalRequest(for request:).

I won’t go into the details of how they work, but by returning true for the first one and the unmodified request for the second one, we’re saying that we want MockURLProtocol to intercept all the requests made by a URLSession.

Then we define a requestHandler.

This closure will take as its input a request and will return an HTTPURLResponse and some Data.

As you can imagine, it’s through this closure that we will define the mocked data that we want the call to return.

Finally, we need to implement the methods startLoading() and stopLoading().

Inside startLoading() we assert that a requestHandler has indeed been provided.

And then we get the mocked response and data from the handler and we send them to the URLSession.

For the method stopLoading(), we can leave an empty implementation because we don’t need to do anything special if the network call gets canceled.

And now we’re ready to use MockURLProtocol to write a unit test for our class UserAPI!

All we need to do is to create a custom URLSession and add our MockURLProtocol to its protocolClasses property.

Then, we inject that custom URLSession into our instance of UserAPI.

And from that, implementing a test is rather straightforward.

First, we create the mockData.

Then we set a closure for the requestHandler that returns this mockData and that also asserts that the URL called is the correct one.

And finally, we make the API call and we assert that the parsed result we get is indeed the one that we expect!

One last thing, we also implement the tearDown() method so that we set the requestHandler back to nil after each test.

And that’s it, thanks to the class URLProtocol we’ve been able to inject a mock into a network call without needing to make any change to our existing code!

All that was needed was the ability to inject a custom URLSession and if that’s not the case with your code, all you need is to add it as an argument to the initializer 😌

That’s all for this article, I hope you’ve enjoyed learning about URLProtocol and that you will be able to use it inside your own project!

Here’s the code if you want to experiment with it:

// UserAPI.swift

import Foundation

struct User: Decodable {
    let firstName: String
    let lastName: String
}

final class UserAPI {
    let endpoint = URL(string: "https://my-api.com")!
    
    let session: URLSession
    let decoder: JSONDecoder
    
    init(
        session: URLSession = URLSession.shared,
        decoder: JSONDecoder = JSONDecoder()
    ) {
        self.session = session
        self.decoder = decoder
    }
    
    func fetchUser() async throws -> User {
        return try await request(url: endpoint.appendingPathComponent("user/me"))
    }
    
    private func request<T>(url: URL) async throws -> T where T: Decodable {
        let (data, _) = try await session.data(from: url)
        return try decoder.decode(T.self, from: data)
    }
}

// MockURLProtocol.swift

import XCTest

class MockURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            XCTFail("No request handler provided.")
            return
        }
        
        do {
            let (response, data) = try handler(request)
            
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            XCTFail("Error handling the request: \(error)")
        }
    }

    override func stopLoading() {}

}

// UserAPITests.swift

import Foundation
import XCTest

@testable import MyApp

class UserAPITests: XCTestCase {
    lazy var session: URLSession = {
        let configuration = URLSessionConfiguration.ephemeral
        configuration.protocolClasses = [MockURLProtocol.self]
        return URLSession(configuration: configuration)
    }()

    lazy var api: UserAPI = {
        UserAPI(session: session)
    }()

    override func tearDown() {
        MockURLProtocol.requestHandler = nil
        super.tearDown()
    }

    func testFetchUser() async throws{
        let mockData = """
        {
            "firstName": "Vincent",
            "lastName": "Pradeilles"
        }
        """.data(using: .utf8)!

        MockURLProtocol.requestHandler = { request in
            XCTAssertEqual(request.url?.absoluteString, "https://my-api.com/user/me")
            
            let response = HTTPURLResponse(
                url: request.url!,
                statusCode: 200,
                httpVersion: nil,
                headerFields: nil
            )!
            
            return (response, mockData)
        }

        let result = try await api.fetchUser()
        
        XCTAssertEqual(result.firstName, "Vincent")
        XCTAssertEqual(result.lastName, “Pradeilles")
    }
}
Previous
Previous

How to easily test In-App Purchases 🛍️

Next
Next

When should you use a hybrid framework? 🤔