loading...

Mock URLProtocol for local unit testing

quangdecember profile image Quang Updated on ・2 min read

Testable iOS networking (2 Part Series)

1) Mock URLProtocol for local unit testing 2) Multiple Network environment configuration in iOS: Staging, proxy and localhost

There are many reasons you might want to mock a network response:

  • You don't want to test with network calls on production server
  • The backend side of your team might not be ready yet
  • There might be hard-to-reproduce cases such as token expired, wrong password, ...

Making a fake network call might also be useful for running unit testing, or creating a workable iOS app (which is not possible with localhost)

Mock URLProtocol is a technique where you overrides initialization of URLSession to make it return your own networking calls:

let urlSession = URLSession(configuration: configuration)

Creating a MockURLProtocol class that can be passed to .protocolClasses of URLSessionConfiguration is where we headed

let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
urlSession.dataTask(with: urlRequest) { data, response, error in
   // ... handle your response
}.resume()

Implementing MockURLProtocol as suggested by Apple in WWDC 2018:

class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data) )?
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    override func stopLoading() {

    }
    override func startLoading() {
         guard let handler = MockURLProtocol.requestHandler else {
            return
        }
        do {
            let (response, data)  = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch  {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
}

Passing your response inside .requestHandler:

MockURLProtocol.requestHandler = {request in 
    let exampleData = """
{
  "base": "EUR",
  "date": "2018-04-08",
  "rates": {
    "CAD": 1.565,
    "CHF": 1.1798,
    "GBP": 0.87295,
    "SEK": 10.2983,
    "EUR": 1.092,
    "USD": 1.2234
  }
}
""".data(using: .utf8)!
    let response = HTTPURLResponse.init(url: request.url!, statusCode: 200, httpVersion: "2.0", headerFields: nil)!
    return (response, exampleData)
}

(an example response from exchangeratesapi)

You can custom MockUrlProtocol above by

  • subclasing MockURLProcotol for each type of request, just overriding startLoading and adding your own .requestHandler:
class MockUnclaimedTokenExpire: MockURLProtocol {
    override func startLoading() {
        MockUnclaimedTokenExpire.requestHandler = { request in
        // new custom response
        }
        super.startLoading()
    }
}
  • Different return result based on handling request

With the builtin tools above, you can complete a workable app despite backend is ready or not. Just remember to switch back to URLSession.shared when you're ready to deploy in production.

Testable iOS networking (2 Part Series)

1) Mock URLProtocol for local unit testing 2) Multiple Network environment configuration in iOS: Staging, proxy and localhost

Posted on Nov 27 '19 by:

quangdecember profile

Quang

@quangdecember

iOS dev, love Swift, love beautiful & well-designed apps, currently an SDK dev

Discussion

markdown guide