How to stub network in iOS

There are times we wish to fake a network event, for example, a network error. However, integrating a 3rd party stub library just for this purpose is not really worthy. This post aims at demonstrating how to stub network. It is not a tutorial on “how to create a stubbing framework”, therefore, some boundary cases will not be covered so that readers could stay focused.

Fundamental

A typical workflow of network requests

A typical workflow to make a network request is:

  1. Create a session URLSession
  2. Create a task associated with the request: let task = session.dataTask(...) { ... }
  3. Start/resume the task by calling task.resume()

Variations of this workflow

Network requests in iOS may behave differently depending on the configuration of the session. By configuration, I mean URLSessionConfiguration.

How to stub

Case study

For debug purpose, we want to create a fake network response for a specific url. For simplicity, we return the response with status code 500.

URLProtocol

This is an abstract class that handles network requests. Note: It is a class although its name sounds like a protocol. By default, there are several subclasses of it each of which takes responsibility for a specific URL scheme (http, ftp, file…): _NSURLHTTPProtocol, _NSURLDataProtocol, _NSURLFTPProtocol, _NSURLFileProtocol, NSAboutURLProtocol.

When a request is made, the app consults these classes. The first one providing true to canInit(with:) will be given to handle that request.

Let’s stub

The core idea of stubbing network lies at:

We will talk about $H_1$ later because it involves a few cases that should be taken into account. Let’s assume that $H_1$ is already done. Then, $H_2$ is quite simple. We just check whether the request was registered to be stubbed or not.

class CustomURLProtocol: URLProtocol {
	private static var stubs: [String: CustomResponse] = [:]
	override open class func canInit(with request: URLRequest) -> Bool {
    	return url != nil && stubs[request.url!.absoluteString] != nil
	}	

	class func addStub(url: URL, response: CustomResponse) {
		stubs[url.absoluteString] = response
	}
	...
}

func stub(url: URL, statusCode: Int) {
	...
	CustomURLProtocol.addStub(url: url, response: CustomResponse(statusCode: statusCode))
}

$H_3$ is achieved by overriding startLoading(). I will not dive into much detail because it is like building a framework. A simple implementation could be like this:

class CustomURLProtocol: URLProtocol {
	...
    override func startLoading() {
        guard let stubResponse = CustomURLProtocol.stubs[request] else { fatalError() } // Should not happen
        
        switch stubResponse {
        case .error(let error):
            client?.urlProtocol(self, didFailWithError: error)
        case .response(let response):
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
            client?.urlProtocolDidFinishLoading(self)
        ...
        }
    }
}

Now comes the crucial part - $H_1$. There are 2 cases to consider: using the shared session, and creating a session with a configuration.

Shared session

The shared session with basic setups is retrieved via URLSession.shared. In that case, we could make out custom URLProtocol subclass visible to the loading system by calling URLProtocol.registerClass(_:).

Note that, the process of consulting these protocol classes take place in the reversed order. The latest one to register will be consulted first.

Session initialized with a configuration

The workflow in this case is a bit different. The app does not lookup the protocol classes we register. Rather, it chooses from the classes stored in URLSessionConfiguration.protocolClasses. URLProtocol.registerClass(_:) does not help now… A solution could be adopted by adding our custom class to configuration.protocolClasses. Make sure to insert it to the top so that it is consulted first.

configuration.protocolClasses = [CustomURLProtocol.self] + configuration.protocolClasses!

Everything is nearly done! The only thing left is to make sure the configuration a session is using has the setup above. Fortunately, fow now, we can only create a configuration by either one of the three following.

let configuration1 = URLSessionConfiguration.default
let configuration2 = URLSessionConfiguration.ephemeral
let configuration3 = URLSessionConfiguration.background(withIdentififer: identifier)

Using a configuration created by URLSessionConfiguration() will throw a crash. I am not quite sure if it’s a bug or by intention, but I am glad it crashes. Thanks to that, we only have to deal with 3 corner cases. To manipulate the configuration, we can swizzle the getter of .default, .emphemeral and the function .background(withIdentififer:). The swizzle code should be run once, when we perform the first stub.

// For demo, I only cover the case of `.default`
let swizzleDefaultSessionConfiguration: Void = {
	let m1 = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.default))
	let m2 = class_getClassMethod(URLSessionConfiguration.self, #selector(URLSessionConfiguration.swizzled_defaultSessionConfiguration))
	if let m1 = m1, let m2 = m2 { method_exchangeImplementations(m1, m2) }
}()

extension URLSessionConfiguration {
	@objc dyamic class function swizzled_defaultSessionConfiguration() -> URLSessionConfiguration {
		let configuration = swizzled_defaultSessionConfiguration()
		configuration.protocolClasses = [CustomURLProtocol.self] + configuration.protocolClasses!
		return configuration
	}
}

Now, all pieces are ready. Glue them together and enjoy!

Another approach

Instead of taking care of the 2 cases above, we could simply swizzle the init funtions of URLSession. We should swizzle the 2 initializers that take a URLSessionConguration as a param. The beauty of this approach is that we no longer need to register our custom class via URLProtocol.registerClass(_:).

// For demo, only `URLSession.init(configuration:)` is swizzled :D
let swizzleURLSession: Void = {
    let m1 = class_getClassMethod(URLSession.self, #selector(URLSession.init(configuration:)))
    let m2 = class_getClassMethod(URLSession.self, #selector(URLSession.swizzled_init(configuration:)))
    if let m1 = m1, let m2 = m2 { method_exchangeImplementations(m1, m2) }
}()

extension URLSession {
    @objc dynamic class func swizzled_init(configuration: URLSessionConfiguration) -> URLSession {
        configuration.protocolClasses = [CustomURLProtocol.self] + configuration.protocolClasses!
        return swizzled_init(configuration: configuration)
    }
}

Further discussion

P/s: TLDR (You can skip this part because details may get you distracted)

Questions remained

There are a couple of things I have not had reasonable explanations for.

Bizarre stuffs

	class func `init`(configuration: URLSessionConfiguration) -> URLSession { ... }

I tried to simulate this situation with a custom class. The logs show a similar result. However, when subclassing that class, Xcode keeps failing to compile due to not being able to check the subclass type. So I think my suspicion is not quite correct, but it’s still reasonable to me :). Anyway, that’s not a big deal!

Finally, if a stubbing framework is what you are looking for, Mockingjay is my recommendation :D.

Reference

  1. Mockingjay source code
  2. NSURLProtocol Tutorial by Ray Wenderlich
  3. Swift core libraries: swift-corelibs-foundation