Introduction

Dans le dernier article, nous avons présenté deux des trois méthodes pour mocker nos appels HTTP à savoir:

  • Mocking à travers l’héritage.
  • Mocking à travers les protocoles.

On a expliqué que la première méthode ne peut pas être appliquée avec Async Await du à des restrictions techniques de l’API et que la meilleur façon jusqu’à maintenant c’est l’utilisation du mocking à base de protocoles. Cependant, il existe une solution bien meilleure que Apple a présenté au WWDC 2018, c’est l’utilisation de URL Loading system a base de URLProtocol.

Qu’est ce que l’URL Loading System?

URL Loading System constitue une interface entre votre application et toute URL qui respecte le(s) protocole(s) HTTP(s). Le système est composé principalement de URLSession et URLSessionConfiguration.

Apple offre derrière le capot de URL Session API et ses subordonnées URLSessionConfiguration, une classe abstraite URLProtocol qui peut intercepter la communication avec toute URL et offre 4 méthodes à implémenter qui sont:

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

Les 4 méthodes permettent de redéfinir selon notre besoin comment notre client HTTP réagit aux différents états de la communication (lancement, blocage, erreur, recevoir les données etc). Mais, c’est à nous ensuite de redéfinir selon notre besoin et indépendamment du type de notre client HTTP (URLSession, Async Await, Alamofire, Moya).

En plus de ces 4 méthodes, URLProtocol nous offre un URLProtocolClient qui permet de communiquer avec l’URL Loading System. Du coup, comment on peut exploiter URLProtocol afin de rendre notre code de test agnostic du code production?

Comment utiliser URLProtocol?

Tout d’abord, renommons notre URLSessionSpy vers URLProtocolStub qui implémentera la classe abstraite URLProtocol.

Au lieu de:

private class URLSessionSpy: URLSessionProtocol

On aura:

private class URLProtocolStub: URLProtocol 

On gardera toujours notre Stub avec les trois réponses attendus:

private static var stub: Stub?
    private struct Stub {
        let error: NSError?
        let data: Data?
        let response:URLResponse?
    }

Afin que notre URLProtocolStub commence ou arrête l’interception des requêtes HTTP , URLProtocol nous offre deux méthodes qui inscrit ou annule notre URLProtocolStub comme classe qui implémente URLProtocol (le nom protocol met en confusion beaucoup de développeurs, n’oubliez pas c’est une classe abstraite qu’on va redéfinir ces méthodes 🙂 ).

Pour des raisons de lisibilité de notre code de test, j’ai opté de mettre l’enregistrement et l’arrêt dans deux méthodes static 🙂

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

static func stopInterceptingRequest() {
    URLProtocol.unregisterClass(URLProtocolStub.self)
}

Nous avons appris jusqu’à maintenant comment initialiser et dé-inscrire notre URLProcolStub. Maintenant passons à l’implémentation des deux premières méthodes.

override class func canInit(with request: URLRequest) -> Bool {
     return true
}

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

La méthode canInit permet d’authoriser URLProtocolStub à intercepter les requêtes HTTP. Par contre, la deuxième méthode permet de retourner la forme canonique d’une requête. En effet, La forme canonique d’une URL constitue l’url d’origine. Si vous n’êtes pas sûr de votre URL alors URLProtocol vous offre une autre méthode qui permet de transformer une URLRequest vers une forme canonique:

canonicalRequest(for:)

Passons à la méthode startLoading() qui permet de gérer tous les résultats que notre Stub peut retourner:

override func startLoading() {
if let error = URLProtocolStub.stub?.error {
     client?.urlProtocol(self, didFailWithError:error)
}

if let data = URLProtocolStub.stub?.data {
    client?.urlProtocol(self, didLoad: data)
}
             
if let response = URLProtocolStub.stub?.response {
     client?.urlProtocol(
           self,
           didReceive: response,
           cacheStoragePolicy: .notAllowed
       )
  }
  
client?.urlProtocolDidFinishLoading(self)
}

Rappelons que notre URLProtocolClient constitue notre “Stub” des différents retours possibles. Pour realiser, ce dernier dispose d’un ensemble de méthodes qui réalisent ce stubbing. En cas d’erreur par exemple: client?.urlProtocol(self, didFailWithError:error). De la même manière en cas de recevoir une URLResponse, notre client nous renvoie cette réponse (ici, on n’a pas autorisé de mettre en cache la réponse retourné).

Finalement, on est doit redéfinir la dernière méthode stopLoading

override func stopLoading() {}

Comment exploiter URLProtocol dans les tests Async Await?

Passons maintenant à comment utiliser URLProtocol dans les tests qu’on a déjà écrit avec Async Await.

func testGetFromURLShouldThrowErrorOnRequestError() async throws {
    URLProtocolStub.startInterceptingRequests()
    let url = URL(string: "http://anyURL.com")!
    let error = NSError(domain: "any error", code: 1)
    URLProtocolStub.stub(error: error, data: nil, response: nil)
    let sut = URLSessionHTTPClient()
     do {
        _ = try await sut.get(from: url)
        } catch let receivedError as NSError {
             XCTAssertEqual(receivedError.domain, error.domain)
             XCTAssertEqual(receivedError.code, error.code)
     }
     URLProtocolStub.stopInterceptingRequest()
 }

Vous remarquez que notre SUT n’a pas besoin d’injecter une session de type URLSessionProtocol. Dans notre test, on attend à ce que notre méthode get(from: URL) déclenche une erreur lorsque l’appel à async await throw une erreur. Pour réaliser ce test, on stub une erreur en implémentant une méthode helper static à l’intérieur de la classe URLProtocolStub:

static func stub(error: NSError? = nil, data: Data? = nil, response: URLResponse? = nil) {
    stub = Stub(error: error,data: data,response: response)
}

Vous remarquez aussi qu’on doit déclencher et arrêter notre URLProtocolStub en appelant au début de notre méthode de test : URLProtocolStub.startInterceptingRequests() et URLProtocolStub.stopInterceptingRequest().

Notre assertion de test est tout simplement une vérification que l’erreur qu’on a déclenché à travers le stubbing est la même qu’on va recevoir après l’appel de la méthode get(from: URL).

Depuis iOS 14, Apple a modifié la structure de l’erreur retourné en ajoutant des informations concernant la dataTask ce qui ne permet pas de faire des comparaisons de genre:

XCTAssertEqual(receivedError, error)

La solution possible qui marche maintenant c’est la comparaison entre les domains et les codes des erreurs.

Vous remarquez que notre code de test est quasi indépendant de async await à minima au niveau du système de mocking. Cependant Async await couple la manière d’écrire les tests. en effet, nous avons des tests qui se terminent par async throws et une manière d’écrire des tests dépendantes avec la nouvelle API. Jusqu’à maintenant la solution unique de garder nos tests agnostic de async await c’est l’utilisation des Continuations qui seront l’object de la troisième partie sur Async await :).

A très bientôt

“Clean code always looks like it was written by someone who cares.” Michael Feathers

Références:

Leave A Comment

Solve : *
14 + 11 =