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 deURLSession
;URLSessionDataTaskFake
qui hérite deURLSessionDataTask
.
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 :
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.
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 :
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 deURLSession
. C'est là que l'appel réseau est simulé.Créez une instance de
URLSessionDataTaskFake
depuisCréerURLSessionFake
.URLSessionFake
dans sa fonctionresume
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 !