A better way of mocking networking
Luke, 27 Sep 2021
Its common to need to mock your network layer for unit testing, and sometimes for builds for testers too. Ive been using the same old technique for some years, which involves mocking URLSession itself. You create a MockURLSession
class of type URLSession, and then you override the init to provide some things you will need, like the data that will be returned. Then you mock the networking call functions - override func dataTask(with ..
so that when your normal network code runs, it sends your local data (from the bundle) straight back, rather than doing actual network calls.
But theres a problem as of iOS13: a warning pops up next to your override init
that says init() was deprecated in iOS13
. You can just carry on and ignore the warnings of course, but obviously you wont be able to do this forever, and warnings are not ideal. So after some investigation here, its impossible to avoid the conclusion that the days of mocking URLSession are, well over really. Theres another way, which I like more: mocking a different class called URLProtocol, which plugs into URLSession. Heres a typical mock of that:
class MockURLProtocol: URLProtocol {
// Dictionary maps URLs to tuples of error, data, and response
static var mockURLs = [URL?: (error: Error?, data: Data?, response: HTTPURLResponse?)]()
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
if let url = request.url {
if let (error, data, response) = MockURLProtocol.mockURLs[url] {
// Theres a mock response specified - return it
if let responseStrong = response {
self.client?.urlProtocol(self, didReceive: responseStrong,
cacheStoragePolicy: .notAllowed)
}
// Theres mocked data specified - return it
if let dataStrong = data {
self.client?.urlProtocol(self, didLoad: dataStrong)
}
// Theres a mocked error - return it
if let errorStrong = error {
self.client?.urlProtocol(self, didFailWithError: errorStrong)
}
}
}
// Done returning our mock response -
self.client?.urlProtocolDidFinishLoading(self)
}
//required
override func stopLoading() {}
}
A typical use of this looks like this:
//the called url which we will set up a local response for:
let urlString = "https://content.guardianapis.com?"
let url = URL(string: urlString)
//the data which will be returned when code makes the call:
let data = TestResponseCreator.testData
//create the HTTPURLResponse that will come back:
let response = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)
// .. and add it to the response array on our mock
MockURLProtocol.mockURLs = [url: (nil, data, response)]
//create a normal URLSession, but using this protocol instead
let sessionConfiguration = URLSessionConfiguration.ephemeral
sessionConfiguration.protocolClasses = [MockURLProtocol.self]
let mockSession = URLSession(configuration: sessionConfiguration)
//use this URLSession where normally, Id just use URLSession.shared
self.articleFetcher.api = MobileAPIClient(baseURL: urlString, session: mockSession)
Theres no more work here than there was in mocking URLSession, and I have no warnings. But also, I like the structure: I can specify easily which called URLs return what data. I could in theory have some calls still doing normal real calls, and some doing fake ones (just by not having that URL in our URL array). But mostly, its just the neatest way around the deprecation of the inits on URLSession.