Stubbing HTTP Response by Using Apple-Authorised Man-in-the-Middle Attack
How URLSession works and how to stub responses by subclassing URLProtocol
iOS apps interact a lot with the network. They read or write state to or from the server, and fetch data, images, audios, and videos from remote. To protect and verify the network layer, we write unit tests around it. Sadly, if we write tests that rely on networking, they will be slow and unstable. And to be fair, they are not really unit tests but more integration tests.
How can you stub the network request and response and isolate the unit test’s code from networking without installing any third-party libraries? Registering your own instance of
URLProtocol at the
URLSession configuration is the key:
urlSessionConfiguration.protocolClasses = [StubURLProtocol.self]
The following explains the code in detail.
1. The Core Flow of the URL Loading System
URLSession plays the core role of the iOS URL Loading System. The instance of
URLSession creates one or more instances of
URLSessionTask, which can be the instances of its subclasses:
URLSessionDataTask: Fetch and return data to your app
URLsessionUploadTask: Upload data and files to the remote
URLSessionDownloadTask: Download data and files from the remote
URLSessionStreamTask: Read and write from and to the remote by using an enqueued and executed serially TCP/IP connection
URLSessionWebSocketTask: Read and write asynchronously from and to the remote by using TCP and TLS in the form of WebSocket framing.
URLSessionConfiguration to configure the instance of
URLSession, which controls the behavior, like how to use caches and cookies or whether to allow connections on a cellular network.
One instance of
URLSession contains only one instance of
URLSessionConfiguration but can create multiple instances of
When the instance of
URLSessionTask reaches its result, it delivers the server’s response, data, and possible errors to the
Commpletion Handler. We verify the error parameter and check the response status code and data there.
Normally, when sending the HTTP request, we use the default instance with the default configuration to create a data task with the request:
2. Register Custom Request Handler to Stub
Although Apple hides the underlying complexity of the URL Loading System under the
URLSession, there are still Hooks to provide configuration when needed. When we register the subclass of
URLProtocol to the configuration of the instance of
URLSession, it gives us the capability to intercept the requests and the responses.
URLProtocol is the most obscure and powerful part of the URL Loading System. It’s an abstract class and has four important and powerful methods to be implemented:
URLSession checks the registered protocols to confirm if it can handle the request when the request is loaded. The registered protocols are consulted in reverse order of when they are registered.
The first protocol to respond
true with the
canInit(with:) function gets to handle the request.
This is where we can alternate the request if needed.
It is up to each concrete protocol implementation to define what canonical means.
This is the most important method to return the stub response. It has the instance of
URLProtocolClient, which is used to communicate with the URL Loading System.
Don’t implement the
URLProtocolClient protocol in your application. Instead, your
URLProtocol subclass calls methods of this protocol on its own
When this method is called, the protocol implementation should end the work of loading a request. This could be in response to a cancel operation, so protocol implementations must be able to handle this call while a load is in progress.
This method is required at the subclass of
URLProtocol but not required to do anything.
3. The sample project
To demonstrate how to implement the flow with an easily extended and testable network layer, I build the sample project against RESTful API service.
The networking protocol
Since we are building the project around stubbing the network response, I’m going to start from the network level.
Every time we come up with a new object, it’s a good idea to start from the protocol. You’ll see that doing this allows you to write code that’s highly testable, flexible, and focused.
Let’s write a protocol definition for the networking, with the following three goals:
- The networking can provide the customized instance of
- The networking accepts the endpoint’s request, executes it with the specified instance of
URLSession, and returns the response in the desired global dispatch queue.
- The networking can decode the data into an appropriate model.
By implementing the extension of
Networking protocol, we have the default implementation.
The DefaultNetworking for app
Now we can have the
DefaultNetworking for normal service usage at the app.
The MockNetworking for unit tests
- We get the ephemeral configuration from
- And then we register the
MockURLProtocolwith the ephemeral configuration.
- We generate the custom instance of
URLSessionfor mock networking.
The MockURLProtocol adopts URLProtocol
- We adopt the
URLProtocolfor the subclass
- There are two properties, one for setting stub error and another for stub response.
- We implement the methods to handle the request and return the stub response.
4. Unit tests With Stub Response
Woohoo, we can stub response for our unit tests now!
To do that, add the sample response as JSON, and pass the encoded data to the request handler. Make the service request by using the
MockNetworking, and enjoy the stub response!
The article describes how we stub response by registering the subclass of
URLProtocol with the
URLSession configuration. The
URLProtocol plays the key role here. It works like the Apple-authorised man-in-the-middle attack.
There is a lot of potential we can explore with the
URLProtocol without changing anything else about how requests are loaded.
Thank you for reading, and please leave any questions you might have in the comments.
All code mentioned above can be found in this GitHub repo.