UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to test iOS networking code the easy way

Faster, easier network tests using URLProtocol

Paul Hudson       @twostraws

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.

Hacking with Swift is sponsored by Proxyman

SPONSORED Proxyman: A high-performance, native macOS app for developers to easily capture, inspect, and manipulate HTTP/HTTPS traffic. The ultimate tool for debugging network traffic, supporting both iOS and Android simulators and physical devices.

Start for free

Sponsor Hacking with Swift and reach the world's largest Swift community!

Controlling network requests

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.

Subclassing URLProtocol

Creating our own URL handler requires creating a subclass or URLProtocol and implementing four methods:

  • The 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.
  • The 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 special
  • The startLoading() method will be called when we need to do our loading, and is where we’ll return some test data immediately.
  • The 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.

What next?

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.

Hacking with Swift is sponsored by Proxyman

SPONSORED Proxyman: A high-performance, native macOS app for developers to easily capture, inspect, and manipulate HTTP/HTTPS traffic. The ultimate tool for debugging network traffic, supporting both iOS and Android simulators and physical devices.

Start for free

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 3.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.