Faster, easier network tests using URLProtocol
It’s commonly agreed that unit tests should be FIRST: Fast, Isolated, Repeatable, Self-verifying, and Timely. Sadly, networking code is never fast and frequently not repeatable, so if you’re writing tests that rely on networking it's fair to say that you’re not really writing unit tests at all.
It’s possible and indeed common to write mocks for the network layer, substituting a mocked URLSession
object for the real one using dependency injection, and that approach works well a lot of the time. However, if you can avoid mocking URLSession
it’s usually a good idea to do so, not least because it’s easy to find yourself in a hole where you’re relying heavily on the way your mock works.
In this article I’m going to show you a smart and simple alternative to network testing that lets you get fast, repeatable unit tests without having to subclass URLSession
or wrap it in a protocol.
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until October 1st.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Apple designed URLSession
to be highly extensible. When you make a request for some sort of data, internally it looks over a series of registered protocol handlers to find the right one to use. For example, if you request a site over HTTP/2, it will find the specific protocol handler for that and make it do the underlying work.
Helpfully, these URL protocols all descend from a common abstract class called URLProtocol
, and we can create our own custom subclass to handle network requests. We can then register that subclass with URLSession
by setting its protocolClasses
property, and it will take part in the standard system networking just like the built-in protocols.
The best part of this is that it’s almost transparent: in your tests you configure a URLSession
instance to use your custom protocol handler, then pass that to your test object to use. Internally, the test object doesn’t know or care that a custom protocol handler is being used – it just calls the URLSession
directly as it normally would, and gets back data.
Apple's official documentation says “the URLSession object searches the default protocols first and then checks your custom protocols until it finds one capable of handling the specified request.” However, that’s quite wrong as you’ll see – if you try requesting a web page using HTTPS you’ll get back the regular data, but if you inject your own protocol handler into the mix that will always get used instead.
Creating our own URL handler requires creating a subclass or URLProtocol
and implementing four methods:
canInit()
method is called to determine whether this handler can handle a specific kind of request. We’ll always return true from this, which means we want to handle all requests.canonicalRequest()
method is designed to convert a regular request into a canonical request. Apple’s documentation says “it is up to each concrete protocol implementation to define what canonical means,” so here we’re just going to return the original request because we don’t need anything specialstartLoading()
method will be called when we need to do our loading, and is where we’ll return some test data immediately.stopLoading()
method is required, but doesn’t need to do anything so we’ll just write an empty method.That’s the absolute minimum a URLProtocol
subclass needs to do, but for our testing purposes we need to add one more thing: a static property that stores all the data we want to return for different URLs.
You see, when startLoading()
is called, we want to look up the URL that was requested and use that to figure out what test data to return. Using a dictionary allows test writers to program their expectations up front: when the user profile page is requested return this, but when the home page is requested return something else.
Let’s put all that into code. Create a new Cocoa Touch class now, calling it “URLProtocolMock” and making it subclass URLProtocol
. Technically what we’re creating is a stub rather than a mock because it’s returning fixed data, so if you want to be technically correct (the best kind of correct!) you can call it “URLProtocolStub” instead.
Regardless of what you call it, when Xcode opens the file for editing give it this code:
class URLProtocolMock: URLProtocol {
// this dictionary maps URLs to test data
static var testURLs = [URL?: Data]()
// say we want to handle all types of request
override class func canInit(with request: URLRequest) -> Bool {
return true
}
// ignore this method; just send back what we were given
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
// if we have a valid URL…
if let url = request.url {
// …and if we have test data for that URL…
if let data = URLProtocolMock.testURLs[url] {
// …load it immediately.
self.client?.urlProtocol(self, didLoad: data)
}
}
// mark that we've finished
self.client?.urlProtocolDidFinishLoading(self)
}
// this method is required but doesn't need to do anything
override func stopLoading() { }
}
With that custom URLProtocol
class in place, we can now create tests that use real URLSession
instances. For example:
// this is the URL we expect to call
let url = URL(string: "https://www.apple.com/newsroom/rss-feed.rss")
// attach that to some fixed data in our protocol handler
URLProtocolMock.testURLs = [url: Data("Hacking with Swift!".utf8)]
// now set up a configuration to use our mock
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [URLProtocolMock.self]
// and create the URLSession from that
let session = URLSession(configuration: config)
With that done, you can use regular dependency injection to send that pre-configured URLSession
object wherever it’s needed. For example, if you had a user
object that wanted to fetch the list of purchases the user had made, you might write this:
user.fetchPurchases(using: session) {
XCTAssertEqual(user.purchaseCount, 0)
expectation.fulfill()
}
All the application code using URLSession
hasn’t changed at all, you haven’t created any sort of heavyweight mock, but you have managed to make networking code fast and repeatable.
Having a static property on a class might seem risky, particularly when using Xcode’s parallel tests. Fortunately, there’s no risk at all: Xcode’s parallel tests are run in isolation, so there’s no chance of one test overwriting the test data by accident – it just isn’t possible.
If you like this approach and want something more advanced, WeTransfer have a GitHub repo called Mocker that might interest you – it lets you return data based on file extension, send specific status codes, and more.
And if you have other suggestions for great ways to test networking code, let me know about them on Twitter – I’m @twostraws there.
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until October 1st.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.