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 !