• 12 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 24/05/2022

Préparez votre double

En introduction de cette partie, je vous ai expliqué que pour tester des appels réseaux, on pouvait utiliser les expectations ou les doubles, et pourquoi nous allions préférer les doubles.

Dans les deux chapitres précédents, nous avons créé nos données de test et préparé notre classe  QuoteService  à être testée. Dans ce chapitre, nous allons créer nos doubles !

Vous avez dit double ?

Un double, c'est un peu le jumeau maléfique d'une classe. Pour notre code côté application, il est invisible, car il ressemble en tout point à la classe qu'il double. Mais en fait, son implémentation interne est complètement différente.

Il existe de nombreux types de doubles : le dummy, le stub, le spy, le mock et le fake.

Sachez seulement que nous allons utiliser ici un fake qui est la version la plus sophistiquée du double, car elle simule complètement le comportement de la classe originale.

Qui doubler ?

On souhaite éviter l'appel réseau et on a vu au chapitre précédent qu'il était préparé avec la méthode  dataTask  de  URLSession  et lancé avec la méthode  resume  de  URLSessionDataTask  .

Nous allons donc doubler ces deux classes responsables conjointement de l'appel réseau. On va donc créer les classes :

  • URLSessionFake  qui hérite de  URLSession  ;

  • URLSessionDataTaskFake  qui hérite de  URLSessionDataTask  .

Quoi doubler ?

Que va-t-on doubler dans nos deux classes ? Autrement dit, quelles méthodes va-t-on doubler ?

La réponse est simple : toutes les méthodes dont notre code a besoin pour fonctionner. Donc si on regarde notre code, on va voir qu'il s'agit pour  URLSessionFake  de :

func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

et pour  URLSessionDataTaskFake  de :

func resume()
func cancel()

Ce sont les seules méthodes de ces deux classes que l'on utilise dans notre code.

C'est parti !

On sait maintenant où on va, alors allons-y !

Côté test, créez un fichier  URLSessionFake.swift  et dedans, créez vos deux classes  URLSessionFake  et  URLSessionDataTaskFake  :

class URLSessionFake: URLSession {
}
class URLSessionDataTaskFake: URLSessionDataTask {
}

URLSessionDataTaskFake

Nous allons commencer par l'implémentation de  URLSessionDataTaskFake  . Nous allons faire les overrides des deux méthodes citées plus haut :

class URLSessionDataTaskFake: URLSessionDataTask {
   override func resume() {}
   override func cancel() {}
}

La fonction  cancel  doit annuler l'appel réseau s'il y en a un en cours. Dans nos tests, comme on simule l'appel, cela aura lieu instantanément, donc il n'y aura jamais d'appel en cours à annuler. Donc on peut laisser son implémentation vide.

La fonction  resume  doit lancer l'appel. Dans notre cas, comme c'est instantané, cette fonction ne va pas lancer l'appel mais appeler directement le bloc de retour avec les données de la réponse.

Quand je parle du bloc de retour, je parle de ceci :

Capture d’écran : le bloc de retour est entouré en rouge.

Ce bloc est une fermeture qui a pour type  (Data?, URLResponse?, Error?) -> Void  . Je vous propose qu'on crée une propriété de ce type.

var completionHandler: ((Data?, URLResponse?, Error?) -> Void)?

Nous allons mettre en propriété les trois paramètres de cette fermeture. Cela va nous permettre, lorsque nous ferons nos tests, de pouvoir configurer les réponses que nous simulerons avec les valeurs de notre choix.

var data: Data?
var urlResponse: URLResponse?
var responseError: Error?

Du coup, la fonction  resume  peut maintenant être rédigée. Il s'agit seulement d'exécuter le bloc de retour avec les paramètres que nous venons d'écrire :

override func resume() {
   completionHandler?(data, urlResponse, responseError)
}

Nous avons maintenant un beau double de  URLSessionDataTask  . Et nous allons pouvoir passer à  URLSessionFake  .

URLSessionFake

Comme on l'a dit en début de chapitre, nous allons faire l'override de deux méthodes ici :

class URLSessionFake: URLSession {
   override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {}

   override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {}
}

Le rôle de ces deux méthodes est de créer une instance de

URLSessionDataTask  qui va contenir toutes les données nécessaires pour faire la requête et ensuite faire la requête avec la méthode  resume  .

Nous allons ici non pas créer une instance de  URLSessionDataTask  mais plutôt de  URLSessionDataTaskFake  .

class URLSessionFake: URLSession {
     override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      return task
   }

 
     override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      return task
   }
}

Maintenant, nous allons configurer notre fausse tâche  task  . Tout d'abord, nous allons lui passer le paramètre  completionHandler  :

class URLSessionFake: URLSession {
     override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      task.completionHandler = completionHandler
      return task
   }

 
     override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      task.completionHandler = completionHandler
      return task
   }
}

Ensuite, réfléchissons un peu. Dans nos tests, nous allons utiliser l'initialisation de  QuoteService  créée au chapitre précédent pour passer à notre objet  QuoteService  de fausses sessions :

init(quoteSession: URLSession, imageSession: URLSession) {
   self.quoteSession = quoteSession
   self.imageSession = imageSession
}

Ces fausses sessions sont donc notre moyen de configurer les réponses de l'appel. Et comme vous le savez, une réponse contient trois données : data, response et error. Donc nous allons faire en sorte que nos  URLSessionFake  soient configurables avec ces trois données :

var data: Data?
var response: URLResponse?
var error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
   self.data = data
   self.response = response
   self.error = error
}

Maintenant, nous allons passer ces données à notre objet  task  :

override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
   let task = URLSessionDataTaskFake()
   task.completionHandler = completionHandler
   task.data = data
   task.urlResponse = response
   task.responseError = error
   return task
}

 
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
   let task = URLSessionDataTaskFake()
   task.completionHandler = completionHandle
   task.data = data
   task.urlResponse = response
   task.responseError = error
   return task
}

Et voilà, nos doubles sont tous prêts et nous allons pouvoir tester !

Par où passe le code ?

J'ai conscience que tout ceci n'est pas évident à digérer alors je vous propose qu'on prenne du recul pour comprendre par où passe le code.

Voici un schéma que je vous propose pour vous y retrouver dans un premier temps. Je vous invite à l'étudier avant de passer à la suite.

Illustration du cheminement du code côté application et côté test.

Dans nos tests, nous allons d'abord créer une instance de  QuoteService  avec l'initialiseur suivant :

let quoteService = QuoteService(quoteSession:URLSession, imageSession:URLSession)

Dans  quoteSession  et  imageSession  , nous allons injecter des instances de  URLSessionFake  que nous allons initialiser comme ceci :

URLSessionFake(data: Data?, response: URLResponse?, error: Error?)

À la place des paramètres  data  ,  response  et  error  , nous allons mettre les données que nous avons préparées dans notre classe  FakeResponseData  .

Ensuite, nous allons appeler la méthode  getQuote  puisque c'est cette méthode que l'on cherche à tester :

quoteService.getQuote()

Cette méthode va s'exécuter et appeler la méthode  dataTask  . Seulement ce ne sera pas la version originale mais la version que nous venons d'écrire, celle d'  URLSessionFake  . Notre version construit une instance de  URLSessionDataTaskFake  et la remplit avec d'une part les données issues de notre classe  FakeResponseData  , et d'autre part le  completionHandler  qui n'est autre que le bloc suivant :

Capture d’écran : le bloc de retour est entouré en rouge.

Ensuite, dans  getQuote  , on appelle la méthode  resume  sur la tâche nouvellement créée :

task?.resume()

Seulement ici notre task est de type  URLSessionDataTaskFake  , et donc la version de la méthode  resume  qui va être appelée est celle que l'on vient d'écrire. Et cette version exécute le bloc (celui de l'illustration ci-dessus) avec les paramètres de réponse que l'on a récupérés de la classe  FakeResponseData  .

Et voilà comment on simule un appel.

Si tout cela n'est pas encore parfaitement clair pour vous, prenez le temps de bien parcourir le code ou de relire les différents chapitres pour comprendre comment interagissent les différentes classes. Pour vous y aider, voici le code complet du fichier  URLSessionFake.swift  :

import Foundation

class URLSessionFake: URLSession {
   var data: Data?
   var response: URLResponse?
   var error: Error?

   init(data: Data?, response: URLResponse?, error: Error?) {
      self.data = data
      self.response = response
      self.error = error
   }

     override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      task.completionHandler = completionHandler
      task.data = data
      task.urlResponse = response
      task.responseError = error
      return task
   }

     override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      let task = URLSessionDataTaskFake()
      task.completionHandler = completionHandler
      task.data = data
      task.urlResponse = response
      task.responseError = error
      return task
   }
}

class URLSessionDataTaskFake: URLSessionDataTask {
   var completionHandler: ((Data?, URLResponse?, Error?) -> Void)?
   var data: Data?
   var urlResponse: URLResponse?
   var responseError: Error?

   override func resume() {
      completionHandler?(data, urlResponse, responseError)
   }

   override func cancel() {}
}

En résumé

Pour créer un double :

  • Créez  un jeu de données qui contient de fausses réponses de l'API dans la classe  FakeResponseData  .

  • Stockez le jeu de données dans un  URLSessionFake  .

  • Injectez dans  QuoteService  l’URLSessionFake, et il remplace l'implémentation de  URLSession  . C'est là que l'appel réseau est simulé.

  • Créez une instance de  URLSessionDataTaskFake  depuis  CréerURLSessionFake  .

  • URLSessionFake  dans sa fonction  resume  exécute le bloc de retour avec les données reçues.

Vous avez préparé votre double, passons maintenant à l’étape suivante : rédiger vos tests !

Exemple de certificat de réussite
Exemple de certificat de réussite