//
you're reading...

Programming

How to Unit Test Network Code in Swift

How to test network code without accessing the server?

Unit tests would need to be:

  • Fast, so that they can be run without impeding productivity.
  • Deterministic, meaning it’ll have the same result every time given there is no code change.
  • Self-Contained, it’ll run on every developer’s machine and on a continuous integration server.

Many iOS applications are dependent to a server connected through the Internet. Either for business logic shared with other platform, central data storage for collaboration, or the app itself is to deliver service provided by the server. Therefore networking is a common — if not central — topic in iOS development.

But unit tests of networking code is challenging. How to create a fast, deterministic, and self-contained test of a component which purpose is to talk to another computer? Have a dedicated “test backend” for the test? Making it deterministic would be difficult — outages, changes, or any issue with the backend server would impact corresponding test. Create a “mini-backend” for the unit test? It won’t be “self-contained” would it? Depending on a running backend would make it a lot harder for the test to work in different developer’s machines (e.g. port number conflicts, executable dependencies, et cetera).

Plugins to the Rescue

Luckily Apple has a URL Loading System that you can plug into. You can intercept URL requests and then provide your own implementation on how that URL is handled. Originally this is meant for custom protocols — non-standard URL schemes for those schemes that are not handled by the system (Gopher, anyone?). However you can use it to override the system’s built-in protocol handlers — in turn to simulate backend responses. You don’t need a running server to test network code, but provide a custom URL handler instead. Your network code doesn’t need to change for the sake of testability — the mock backend can be transparent to your network code.

In both macOS and iOS (also implies iPadOS), custom protocols revolve around the URLProtocol class. Subclass this to implement a custom URL scheme or provide an alternative implementations of how URLs with a certain pattern are handled. Custom URL handlers are not limited to customizing the schemes – you can choose to override handling based on host names, paths, or anything that can be identified from a URL object.

These are the two core URLProtocol methods to override in your own subclass when implementing a custom protocol:

  • canInit(with:) — class method that tells the system whether it can handle a given URL.
  • startLoading() — performs the work of loading the URL.

Be sure that your implementation of canInit(with:) is wickedly fast. Because the system would call that in a chain along with other subclasses of URLProtocol for every URL request. Make decisions on what is given in the URL, don’t modify anything (make the method idempotent), and don’t do any I/O. Otherwise you’ll risk slowing down all URL requests.

In turn startLoading() would need to initiate processing of the URL and keep the URLProtocolClient delegate object informed on the process:

Custom URL Loading System

There other delegate methods of URLProtocolClient. But the above four should cover the common case.

There are two ways you can put a URLProtocol subclass into use:

  • Global registration – enable it for all URL requests in the application.
  • Per-session registration – only use it in specific URLSession objects.

Call the registerClass(_:) class method to enable a URLProtocol subclass globally. Alternatively include the class object inside protocolClasses property of URLSessionConfiguration when creating a URLSession object that can use the customized URL loading protocol.

Unit Testing with URLProtocol

Unit testing network code starts with a URLProtocol subclass that intercepts the network requests that the code-under-test is making. Instead of going to the backend, instead the URLProtocol subclass would return a result that the backend is expected to provide given a certain request.

If you can, try to configure the custom URLRequest subclass per-session instead of globally. It would make things easier to debug later on. Have your network class (or struct) accept a URLSessionConfiguration instance upon construction and use it to create all URLSession objects that it needs. In turn, unit test suites would provide URLSessionConfiguration containing the custom URLProtocol subclasses when the network class is run under test.

Sample Test

The following is an example unit test, which tests for an HTTP not found error returned by the backend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func testFailure() {
    let testName = "not-found.json"
    let targetURL = baseURL.appendingPathComponent(testName)
    BlockTestProtocolHandler.register(url: targetURL) { (request: URLRequest) -> (response: HTTPURLResponse, data:Data?) in
        let response = HTTPURLResponse(url: request.url!, statusCode: 404, httpVersion: BlockTestProtocolHandler.httpVersion, headerFields: nil)!
        return (response,Data())
    }
    
    let fetchCompleted = XCTestExpectation(description: "Failure Fetch Completed")
    defer {
        self.wait(for: [fetchCompleted], timeout: 7)
    }
    let client = makeExampleNetworkClient()
    client.fetchItem(named: testName) { (data: Data?, error: Error?) in
        defer {
            fetchCompleted.fulfill()
        }
        XCTAssertNotNil(error, "Error expected")
        XCTAssertNil(data, "Data expected")
        if let cocoaError = error as NSError? {
            XCTAssertEqual(cocoaError.domain, ExampleNetworkClient.ErrorDomain, "Unexpected error domain")
            XCTAssertEqual(cocoaError.code, ExampleNetworkClient.ErrorCode.invalidStatus.rawValue, "Unexpected error code")
        }
    }
}
  1. Lines 5–8 begins the test by registering a custom block handler which returns the HTTP 404 error into BlockTestProtocolHandler which is a custom URLProtocol subclass.
  2. At line 15 the under-test object gets created.
  3. Lines 16–25 invokes the method under test ad asserts its output.
  4. Since this is an asynchronous code, lines 11–13 blocks the test until the callback result handler gets invoked.

Under-test objects are created via the makeExampleNetworkClient() method of the unit test suite. This is where the custom URLSessionConfiguration object configures the custom URL handlers into it.

1
2
3
4
5
6
7
func makeExampleNetworkClient() -> ExampleNetworkClient {
    let config = URLSessionConfiguration.ephemeral
    config.protocolClasses = [
        BlockTestProtocolHandler.self
    ]
    return ExampleNetworkClient(baseURL: baseURL, configuration: config)
}

You can find the full example project from my Github account.

Next Steps

Start unit-testing your network code. Notably code which parses JSON or XML coming from a backend. A good strategy is to take an example output and save it as a resource file along with the corresponding unit test case. Then the URLProtocol subclass for unit testing can return this resource file instead as part of the test case.

When you commit an XML or JSON file into a source repository, remember to normalize and pretty-print it beforehand. Normalizing a JSON file means to sort dictionaries by its keys (but it doesn’t mean re-ordering arrays) and then pretty-print it. Thus changing a key or value would show as a line change in the corresponding diff. There should be a similar process to prepare inclusion of XML files in source control.

That is all for now. Please let me know if you have more tips on unit testing I/O code.



Do you enjoy this post? Enter your e-mail address in the form below to receive:

  • A cheat sheet on how to pass App Review Guideline 4.2 “Minimum Functionality”.
  • Notifications of new articles as soon as they are published.
  • Occasional tips and updates about my work.

You can unsubscribe any time and I won’t share your e-mail to any third party.

* indicates required

Discussion

Trackbacks/Pingbacks

  1. […] View Reddit by jtsakiris – View Source […]

  2. […] Testing network I/O code without accessing the backend is not easy. Here is how you can mock the backend by injecting code into the URL Loading System… Read more […]

Leave a Reply

%d bloggers like this: