Saturday, July 27, 2024
HomeProgrammingEasy methods to Check Your Community Connection Requests in Swift Utilizing URLProtocol...

Easy methods to Check Your Community Connection Requests in Swift Utilizing URLProtocol | by Matias Glessi


Photograph by Marvin Meyer on Unsplash

On this article, we’ll see the best way to take a look at community requests utilizing the not-so-well-known URL Loading System, which intercepts requests made to the server. To handle our downside, we’ll put some instance code, assuming we now have an implementation of this type in our productive code:

enum HTTPResult {
case success(Knowledge, HTTPURLResponse)
case failure(Error)
}

protocol HTTPClient {
func get(from URL: URL, completion: @escaping (HTTPResult) -> Void)

class URLSessionHTTPClient: HTTPClient {
non-public let session: URLSession

init(session: URLSession = .shared) {
self.session = session
}

func get(from url: URL, completion: @escaping (HTTPResult) -> Void) {
session.dataTask(with: url) { _, _, error in
if let error = error {
completion(.failure(error))
}
}.resume()
}
}

Right here, we see a easy instance of an interface to an HTTP consumer known as HTTPClient. That is made up of a single methodology that receives a URL and returns a outcome which generally is a success (with its corresponding Knowledge and Response) or a failure (with an Error).

On the similar time, it has a URLSessionHTTPClient implementation, which is liable for speaking with the community. In our case, we use URLSession, Apple’s framework for community requests. As the main focus is on understanding the best way to take a look at this technique element, we’ll go away an implementation already finished. Nonetheless, this may very well be created from the take a look at choices following Check-Pushed Improvement (TDD).

First, we’ll see some various methods to check implementation, which, though legitimate, have some disadvantages to contemplate, which can information this put up.

One method to resolve this may very well be to check the connection for actual. The request is made to the backend, the response is obtained, and it’s evaluated whether it is appropriate. Though it’s a legitimate possibility, we simply discover a number of explanation why this technique may be problematic: What if the backend will not be developed but? How can we deal with the a number of causes for which a connection can fail? Easy methods to improve the take a look at period if the connection is simply too gradual? Though it’s legitimate as a method, testing the element in isolation might be higher.

Testing the service end-to-end method could be extra helpful if it took a number of elements and the way they’re built-in.

Since our implementation will use the Apple URLSession framework to make connections to the server, one technique could be to mock it, implementing a subclass of it that may spy on or seize the data wanted to validate our checks. For instance, we may add flags to test that the strategies had been known as, save sure values comparable to URLs despatched and even the URLSessionDataTask used (mocking these, too), and validate they’re appropriate.

The issue with this technique is that since it’s a subclass of an Apple framework, there are lots of strategies that we aren’t even conscious of, which we should always implement if we need to have full management of the category. In any other case, our checks might find yourself utilizing the mother or father class strategies, which is harmful since we might not make certain the way it works (or if community requests are made on this particular case). In every launch, even Apple can add new strategies or replace the outdated ones, altering how they work and inflicting our checks to fail.

A 3rd possibility to deal with the problem is to create protocols that mimic the interfaces we’re considering mocking. For instance, we may create one thing like this:

protocol HTTPSession {
func dataTask(
with url: URL,
completionHandler: @escaping (Knowledge?, URLResponse?, Error?) -> Void)
-> HTTPSessionTask
}

protocol HTTPSessionTask {
func resume()
}

Thus, we might have a protocol similar to URLSession (and one other much like URLSessionDataTask) that may solely have the tactic that we’re considering mocking. Within the take a look at, our SUT will work together with the created protocol as a substitute of the URLSession. This enables us to keep away from assumptions about unknown strategies and secret Apple implementations of how issues work, on this case, relating to the URLSession API. It additionally saves us the necessity to replace these checks sooner or later if Apple decides to replace their strategies since we solely implement strategies we care about.

Whereas that is one other legitimate technique, one other downside will not be the perfect resolution: since we’re mimicking the URLSession strategies, there’s a sturdy coupling with this API. Additionally, we’re including productive code to fulfill our checks, which is certainly a wake-up name.

In keeping with Apple’s definition, the URL Loading System lets you work together with URLs and talk with servers utilizing commonplace protocols (comparable to HTTP/HTTPS, for instance) or with your personal protocols you’ll be able to create.

URL Loading System diagram — picture by creator

How does it work? Each time a request is made, what occurs behind the scenes is that there’s a system (the URL Loading System) that processes it. As a part of it, there’s a kind known as URLProtocol, which is an summary class that inherits from NSObject.

So if we create our personal URLProtocol and register it, we are able to begin intercepting URL Requests. What’s it for? On this case, we may consider the element utilizing a selected protocol, implement some Cache, observe info for Analytics, and even consider the efficiency of the requests.

For this, we solely must implement the strategies of the summary class URLProtocol, which, though unusual, is a category. On this case, we’ll create a mock that implements this clsass and consider the validity of the examined requests with the understanding that the requests are by no means made. No info is shipped to any server, making the checks sooner and extra dependable.

We’ll create our personal because the URL Loading System processes requests by way of totally different protocols. This subclass of URLProtocol may have the target of intercepting the transmitted info and validating it.

non-public class URLProtocolStub: URLProtocol { ... }

Since we need to intercept the data from a URLRequest, we may retailer this info in a construction inside our URLProtocolStub. A dictionary may very well be an excellent possibility:

non-public static var stubs = [URL: Stub]()

non-public struct Stub {
let knowledge: Knowledge?
let response: URLResponse
let error: Error?
}

Thus, when making ready our take a look at case within the a part of its preparation, we are able to save this info after which carry out the corresponding checks. One thing like:

 let urlProtocol = URLProtocolStub()
urlProtocol.stub(url: url, knowledge: nil, response: nil, error: error)

The place the stub(url: knowledge: response: error:) methodology of URLProtocolStub may have a type much like this:

func stub(url: URL, knowledge: Knowledge?, response: URLResponse?, error: Error?) {
stubs[url] = Stub(knowledge: knowledge, response: response, error: error)
}

Now that we perceive the best way to retailer the data, we are able to create a take a look at case. We’ll take a look at the error case when the request has an error (error will not be nil), and the consumer ought to return a .failure(error) outcome with the identical error. It will likely be roughly like this:

func test_onHTTPClientGetCall_failsOnRequestError() {
let url = URL(string: "http://any-url.com")!
let error = NSError(area: "any error", code: 1)
let urlProtocol = URLProtocolStub()
urlProtocol.stub(url: url, knowledge: nil, response: nil, error: error)

let sut = URLSessionHTTPClient()

let exp = expectation(description: "Await completion")

sut.get(from: url) { end in
swap outcome {
case let .failure(receivedError as NSError):
XCTAssertEqual(receivedError, error)
default:
XCTFail("Anticipated failure with error (error), obtained (outcome) as a substitute")
}

exp.fulfill()
}

wait(for: [exp], timeout: 1.0)
}

Within the code above, we see our first take a look at case. Within the preparation half, we’ll create a URL, a selected Error, and an occasion of our URLProtocolStub, the place we’ll add the corresponding stub for that request with an error (and its different parts as nil). We’ll then create an occasion of the consumer and make an asynchronous name (with expectation).

Lastly, we validate that the error acquired is similar because the one despatched by way of an XCTAssertEqual(,), within the case of .failure(). In some other case, it’s an surprising outcome error.

If you happen to’re testing this within the IDE, it’s seemingly that nothing is compiling. This occurs as a result of we left the implementation of our Stub within the center. We add that URLProtocolStub will probably be a subclass of URLProtocol, however we don’t implement its necessities, that are primarily two. On the one hand, we should implement 4 strategies of URLProtocol:

class func canInit(with:URLRequest) -> Bool
class func canonicalRequest(for:URLRequest)
func startLoading()
func stopLoading()

Then again, we should register our protocol utilizing the strategies:

URLProtocol.registerClass(AnyClass)
URLProtocol.unregisterClass(AnyClass)

Don’t fear, we’ll take a look at each necessities intimately beneath.

Let’s assessment every of the strategies we should override to satisfy the URLProtocol necessities. The primary one will probably be canInit(with: URLRequest) -> Bool. If we return true on this methodology, we are able to course of this request, and it will likely be our duty to finish the request with success or failure. We will know if the urlRequest accommodates the mandatory components to do it.

How can we all know if we are able to course of this request?

override class func canInit(with request: URLRequest) -> Bool {
guard let url = request.url else { return false }

return stubs[url] != nil
}

Mainly, as we’re storing in a dictionary the data of that request (our Stub aspect) listed by way of the corresponding URL, what’s going to inform us whether or not it will probably course of this request will probably be decided by whether or not we now have the stub saved within the dictionary.

However, if we add that methodology in XCode, we may have an error of the next kind:

Occasion member 'stubs' can't be used on kind 'URLSessionHTTPClientTest.URLProtocolStub'

It is because canInit is named a category methodology as a result of we don’t have an occasion but. The URL Loading System will instantiate the URLProtocolStub provided that the request may be processed. Since we don’t have an occasion, we have to make some modifications.

First, our stubs dictionary needs to be outlined as static:

non-public static var stubs = [URL: Stub]()

So ought to the “stub” methodology:

static func stub(url: URL, knowledge: Knowledge?, response: URLResponse?, error: Error?) { ... }

Lastly, in our take a look at case, we should change the instantiation of the Stub, which might be like this, with out creating any occasion:

URLProtocolStub.stub(url: url, knowledge: nil, response: nil, error: error)

Transferring on, the following methodology we have to implement is canonicalRequest(for request: URLRequest) -> URLRequest. This methodology returns a canonical model of the request, as described within the Apple documentation. It’s often sufficient to return the identical request since we shouldn’t make any modifications to it, however perhaps should you wished so as to add a header or change the URL scheme (for instance), it could be an excellent place to do it. In our case, it will likely be merely:

override class func canonicalRequest(for request: URLRequest) -> URLRequest { 
return request
}

Then we now have to override the startLoading() and stopLoading() strategies, that are occasion strategies. Which means they’re executed as soon as it’s accepted, that it’s going to course of the request, and the mandatory occasion will probably be generated.

We begin with startLoading(). Right here when this methodology is named, the URLProtocolStub implementation ought to begin loading the request:

override func startLoading() {
guard let url = request.url, let stub = URLProtocolStub.stubs[url] else { return }

if let knowledge = stub.knowledge {
consumer?.urlProtocol(self, didLoad: knowledge)
}

if let response = stub.response {
consumer?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}

if let error = stub.error {
consumer?.urlProtocol(self, didFailWithError: error)
}

consumer?.urlProtocolDidFinishLoading(self)
}

Right here, we get the request’s URL (the place the request is an occasion variable) and our stub for that URL. With the guard, allow us to make sure that we now have them, and if not, we end the execution. With the next if assertion, if we get an error, we have to inform the URL Loading System that an error occurred, and we do that with one other property of the URLProtocol occasion, which is the consumer of kind URLProtocolClient. That is an object that the protocol makes use of to speak with the URL Loading System. This consumer has many strategies; one tells the system that it failed with an error through urlProtocol(URLProtocol, didFailWithError: Error).

We are able to test for the existence of information in our stub, and we are able to inform the consumer to load “knowledge” by way of urlProtocol(URLProtocol, didLoad: Knowledge).

Equally, we test for a “response”, which we’ll do by way of urlProtocol(URLProtocol, didReceive: URLResponse, cacheStoragePolicy: URLCache.StoragePolicy). On this case, we additionally ship the Cache coverage, which, since we didn’t cope with it on this article, we’ll ship it as .notAllowed. Lastly, as soon as we end, we should inform the consumer we completed the method with urlProtocolDidFinishLoading(URLProtocol).

The final methodology we have to implement is stopLoading(), the place the cease loading of a request is processed. For instance, this may very well be used to deal with a response to a cancellation. On this case, we received’t add an implementation so that it’s going to appear like this:

override func stopLoading() { }

It is very important implement it at the least empty. In any other case, we may have a crash at runtime. So, we full the implementation of our stub. It needs to be like this:

non-public class URLProtocolStub: URLProtocol {
non-public static var stubs = [URL: Stub]()

non-public struct Stub {
let knowledge: Knowledge?
let response: URLResponse?
let error: Error?
}

static func stub(url: URL, knowledge: Knowledge?, response: URLResponse?, error: Error?) {
stubs[url] = Stub(knowledge: knowledge, response: response, error: error)
}

static func startInterceptingRequests() {
URLProtocol.registerClass(URLProtocolStub.self)
}

static func stopInterceptingRequests() {
URLProtocol.unregisterClass(URLProtocolStub.self)
stubs = [:]
}

override class func canInit(with request: URLRequest) -> Bool {
guard let url = request.url else { return false }

return URLProtocolStub.stubs[url] != nil
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}

override func startLoading() {
guard let url = request.url, let stub = URLProtocolStub.stubs[url] else { return }

if let knowledge = stub.knowledge {
consumer?.urlProtocol(self, didLoad: knowledge)
}

if let response = stub.response {
consumer?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}

if let error = stub.error {
consumer?.urlProtocol(self, didFailWithError: error)
}

consumer?.urlProtocolDidFinishLoading(self)
}

override func stopLoading() {}
}

URLProtocol subclasses should not identified to the URL Loading System simply because they exist. We should register them earlier than a request is made, and thus it will likely be seen to the system, looking for the totally different current protocols and attempting to course of the request with every considered one of them.

To do that, it’s essential to name the registerClass(AnyClass) class methodology that registers the protocol. Equally, we are able to unsubscribe our URLProtocolStub with the unregisterClass(AnyClass) methodology. By including these two traces at the beginning and finish of the take a look at case, we might make it clear to the URL Loading System that we would like it to make use of our Stub. Our take a look at case would appear like this:

func test_onHTTPClientGetCall_failsOnRequestError() {
URLProtocol.registerClass(URLProtocolStub.self)
let url = URL(string: "http://any-url.com")!
let error = NSError(area: "any error", code: 1)
URLProtocolStub.stub(url: url, knowledge: nil, response: nil, error: error)

let sut = URLSessionHTTPClient()

let exp = expectation(description: "Await completion")

sut.get(from: url) { end in
swap outcome {
case let .failure(receivedError as NSError):
XCTAssertEqual(receivedError, error)
default:
XCTFail("Anticipated failure with error (error), obtained (outcome) as a substitute")
}

exp.fulfill()
}

wait(for: [exp], timeout: 1.0)
URLProtocol.unregisterClass(URLProtocolStub.self)
} let sut = URLSessionHTTPClient()

If we run the take a look at case, it ought to go with out issues. Yay!

Though we may additionally add different take a look at instances the place we take a look at different methods, as with .success(), it’s a good place to begin to get into using URLProtocol.

By performing checks on this method, we are able to keep away from assumptions about behaviors that we’re mocking within the checks, that could be unpredictable in manufacturing, and even which will change sooner or later, modifying the character of what we’re evaluating. Additionally, we solely add productive code if we want it for testing, which is often not an excellent signal.

References

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments