My notes on four different network testing strategies. End-to-end testing, mocking by subclassing, mocking with protocols, and URL protocol stubbing.
End-to-end testing is a way to check the integration between client and server communication by using real HTTP requests.
A downside to it is that if there is no backend to talk to yet, it could be a blocker to perform testing. Also, because it requires a network connection to run, it can be slow and flaky depends on network performance. Network request may fail and the test could fail unexpectedly.
This is another alternatives to test at the component level without hitting a real HTTP request by subclassing the URLSession and spying or stubbing its methods. It could be faster and more reliable than end-to-end testing strategy, but it also can be dangerous when you subclass a type you don't own. In here URLSession is a class that you don't have access to its implementation. Another thing is that URLSession and its collaborators (e.g. URLSessionTask) expose a lot of method that we are not overriding in the mock, which increase the risk of wrong assumption and future runtime crashes.
Another downside is the tight coupling between the test and production code. For example to assert a behaviour, we need to assert the precise method calls like assert that a data task is created with a given URL using specific API, then assert that resume is called to start the request, and then only able to assert the behavior we expect.
If possible, decouple test and production code so that production code can easily be changed or refactored without breaking the tests.
Another approach is to mimic the desired interfaces we'd like to spy on by creating a protocol with only specific URLSession method we care about. Then URLSessionHTTPClient collaborates with the protocol instead of the concrete URLSession type. This way, it hides the unnecessary details about URLSession APIs an also avoid overriding any methods.
So mocking with protocols may solve the problem of assumption encountered in mocking with subclassing strategy, but it still tightly coupled with URLSession APIs because we mimicking its method signatures. Moreover, by adding protocols solely for testing, we introduce a lot of noise in our production code.
The last and most preferred approach to test network request is by using URL Loading System to intercept and handle requests with URLProtocol stubs because it solves the problems of the previous three strategies.
Why is this the most preferred way? it's fast and reliable, Apple recommend to use this API to test network request, it helps to decouple the tests from production implementation, and also it hides the tests details from production code.
Interact with URLs and communicate with servers using standard Internet protocols. The URL Loading System provides access to resources identified by URLs, using standard protocols like https or custom protocols you create. Loading is performed asynchronously, so your app can remain responsive and handle incoming data or errors as they arrive. — Apple developer documentation1
URLProtocol is an abstract class that can be implemented by subclasses to handle the loading of protocol-specific URL data. Use it as a mechanism to intercept outgoing requests and altering URL loading behavior.
Here are the required method when subclassing URLProtocol:
class ProtocolStubs: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {}
override func stopLoading() {}
}More references you can find for Testing tips & tricks WWDC, URLProtocol, URLSession
The above quote is excerpted from Apple developer documentation ↩