• 12 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/24/22

Préparez votre classe à être testée

Nous avons maintenant les données qui vont nous permettre de simuler les réponses à nos appels réseaux.

Le but de tout ceci, ne l'oublions pas, est de tester la classe  QuoteService  . Or, lorsqu'on teste, on ne s'intéresse qu'à la partie publique d'une classe, et pas à son implémentation interne.

Dans notre cas, voici à quoi ressemble la partie publique de notre classe :

class QuoteService {
   static let shared: QuoteService
   func getQuote(callback: @escaping (Bool, Quote?) -> Void)
}

Notre interface publique ne présente donc qu'une méthode : la méthode  getQuote  . C'est donc exclusivement cette méthode que nous allons tester.

Le problème, c'est que dans cette méthode, il y a un appel réseau. Or nous voulons justement éviter l'appel réseau dans les tests, et récupérer plutôt les données via notre classe  FakeResponseData  .

À première vue, cela paraît compliqué. Parce que cela veut dire qu'il nous faut deux versions différentes d'une même méthode, une pour les tests et une pour l'application.

Mais c'est impossible !

Pas tout à fait, et nous allons voir comment faire ça dans ce chapitre !

À la recherche des coupables

Regardons un peu notre fonction  getQuote  pour voir comment elle fonctionne :

func getQuote(callback: @escaping (Bool, Quote?) -> Void) {
   let request = createQuoteRequest()
   let session = URLSession(configuration: .default)
   task?.cancel()
   task = session.dataTask(with: request) { (data, response, error) in
      // (...)
   }
   task?.resume()
}

Ici l'appel est créé à la ligne 6 avec la méthode  dataTask  de  URLSession  , et il est lancé à la ligne 9 avec la méthode  resume  de  URLSessionDataTask  . C'est précisément à ces deux endroits que l'on doit modifier les choses pour que l'appel n'ait pas lieu dans les tests.

Donc cela veut dire qu'en fait, on ne veut pas modifier l'implémentation de  getQuote  mais celles de  dataTask(with:, completionHandler:)  et de  resume()  .

Mais on ne peut pas modifier les méthodes de ces classes ! On ne les a pas écrites !

Oui, vous avez tout à fait raison.

Mais rien ne nous empêche de créer des sous-classes d'  URLSession  et de  URLSessionDataTask  dans lesquelles nous allons faire l'override des méthodes qui nous ennuient. Comme ceci par exemple :

class URLSessionFake: URLSession {
     override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
      // Ici j'écris ce que je veux
   }
}

Et dans cette version fake de URLSession, on peut écrire ce qu'on veut.

OK, c'est très bien vu, mais ça ne nous avance à rien !

Pardon ?

Bah oui. Le code de  getQuote  utilise la classe  URLSession  et pas ta classe  URLSessionFake  .

C'est vrai mais on peut changer le code et utiliser la classe  URLSessionFake  , non ?

Oui mais si on fait ça, ce sera bien pour les tests mais l'application ne marchera plus !

Hmmm... Vous vous ne vous laissez pas avoir si facilement, c'est bien ! En effet, il faut qu'on trouve un moyen d'utiliser  URLSession  côté application et  URLSessionFake  côté test.

Et ce problème a une solution qui porte un nom repoussant : l'injection de dépendance.

Découvrez l'injection de dépendance

L'injection de dépendance, dit comme ça, ça a l'air hyper technique mais en fait, c'est vraiment simple. Il s'agit de sortir une variable de l'implémentation d'une méthode pour en faire une propriété.

En tant que propriété de la classe, celle-ci peut-être librement modifiée, et donc le code ne dépend plus de cette variable.

Prenons un exemple pour y voir plus clair. Pour l'instant, mon code ressemble à ceci :

func getQuote(callback: @escaping (Bool, Quote?) -> Void) {
   let request = createQuoteRequest()
   let session = URLSession(configuration: .default) // Le code dépends de URLSession
   task?.cancel()
   task = session.dataTask(with: request) { (data, response, error) in
      // (...)
   }
   task?.resume()
}

Donc mon code dépend de la classe  URLSession  .

Nous allons maintenant extirper cette variable et en faire une propriété de la classe :

class QuoteService {
   var session = URLSession(configuration: .default)
   func getQuote(callback: @escaping (Bool, Quote?) -> Void) {
      let request = createQuoteRequest()
      task?.cancel()
      task = session.dataTask(with: request) { (data, response, error) in
         // (...)
      }
      task?.resume()
   }
}

La dépendance envers URLSession a disparu de l'implémentation de getQuote. Je n’avais pas dit que c'était simple ?

Attends un peu ! La méthode  getQuote  ne dépend peut-être plus de  URLSession  , mais la classe  QuoteService  en dépend toujours.

Certes, mais comme  session  est devenue une propriété, cela signifie que je peux changer sa valeur. Et je peux donc écrire ceci:

var quoteService = QuoteService.shared
quoteService.session = URLSessionFake()

Ainsi, lorsque j'utilise ma classe côté application, je garde la valeur de session par défaut ; mais lorsque je l'utilise pour les tests, j'utilise une autre valeur avec la classe  URLSessionFake  , qui me permet d'éviter l'appel.

Cette propriété est un point d'entrée pour injecter une dépendance. Je peux choisir la dépendance que j'injecte :  URLSession  ou  URLSessionFake  , d'où le nom d'injection de dépendance.

Je vais créer deux sessions différentes, une par appel, donc je vais créer deux propriétés que je vais utiliser dans les deux méthodes  getQuote  et  getImage  :

var quoteSession = URLSession(configuration: .default)
var imageSession = URLSession(configuration: .default)

 
func getQuote(callback: @escaping (Bool, Quote?) -> Void) {
   let request = createQuoteRequest()
   task?.cancel()
   // On utilise quoteSession ici
   task = quoteSession.dataTask(with: request) { (data, response, error) in
      // (...)
   }
   task?.resume()
}

 
private func getImage(completionHandler: @escaping ((Data?) -> Void)) {
   task?.cancel()
   // On utilise imageSession ici
     task = imageSession.dataTask(with: QuoteService.pictureUrl) { (data, response, error) in
      // (...)
   }
   task?.resume()
}

Et voilà, vous avez fait vos deux premières injections de dépendance ! C'était pas si dur, non ?

Codez proprement

Je n'aime pas trop avoir des propriétés publiques qui ne devraient pas l'être. Pourtant les tests ont besoin d'accéder à ces propriétés. Mais je vous propose du coup de créer plutôt un initialiseur pour ces deux propriétés et de les laisser privées.

private var quoteSession = URLSession(configuration: .default)
private var imageSession = URLSession(configuration: .default)

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

Cela limite la modification de ces propriétés à l'initialisation, et non pendant toute la vie de l'objet.

En résumé

  • Pour simuler un appel réseau, vous devez l’imiter en réalisant une injection de dépendance.

  • Pour réaliser une injection de dépendance :

    • Créer une classe qui hérite de  URLSession  afin d’imiter celle-ci.

    • Changer l’instance de  URLSession  en une propriété de classe.

    • Initialiser la classe à tester avec la classe de type  URLSession  que vous souhaitez. C'est-à-dire, la vraie ou la fausse.

Votre classe est désormais prête à être testée ! Dans le prochain chapitre, nous allons créer notre fameuse classe URLSessionFake qui va faire office de fausse session !

Example of certificate of achievement
Example of certificate of achievement