DEV Community

Quang
Quang

Posted on • Edited on

Mock URLProtocol for local unit testing

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.

Top comments (0)