Unit-Test Asynchrone
Dernière mise à jour
Dernière mise à jour
La fonction de "callback" associée au setTimeout
n'est appelée qu'au prochain "tick", après l'exécution de la "spec".
L'assertion est donc ignorée car Jasmine n'arrive à l'associer à aucun test.
done
👎Pour indiquer à Jasmine que la "spec" est asynchrone, il faut ajouter le paramètre done
à la fonction de "spec".
On obtient alors l'erreur suivante :
Par défaut, la variable jasmine.DEFAULT_TIMEOUT_INTERVAL
vaut 5 secondes.
N'augmentez jamais cette valeur !
Un test unitaire doit être F.I.R.S.T. :
Fast
Independent
Repeatable
Self-Validating
Thorough & Timely
La fonction done
doit être appelée explicitement à la fin de la "spec".
Cette fois-ci, la "spec" échoue rapidement à cause de l'assertion.
Cette approche s'avère rapidement pénible à mettre en place et surtout "Error-Prone".
Elle finit rapidement en Callback Hell et "Race Conditions".
Promise
et async / await
✌️Plutôt que d'utiliser la fonction done
, la fonction de "spec" peut retourner une Promise
dont la résolution signale la fin.
Plus simple et plus sexy, il est possible d'utiliser async / await
.
async()
🤟La fonction Angular async
(à ne pas confondre avec la syntaxe ECMAScript async / await
) est une fonction dédiée aux tests Angular.
En créant une "Zone" (Cf. Zone.JS) autour de la "spec" et grâce au "Monkey Patching" de toutes les sources d'exécution asynchrone (setTimeout
etc...), la fonction async
retourne une fonction de "spec" asynchrone et appelle la fonction done
quand tous les traitements asynchrones détectées sont terminés (i.e. la queue de l'Event Loop et vide et plus aucun traitement en attente).
Cette approche a pour avantage :
d'être simple d'utilisation,
d'être moins "Error-Prone" que les approches précédentes,
de garantir que Jasmine ne passera à la "spec" suivante que quand tous les traitements asynchrones seront terminés.
Testons cette station météo capricieuse :
Bien que l'assertion expect(temperature).toEqual(-10)
soit erronée, la "spec" réussit.
En effet, le "pipe" filter(_city => _city !== 'Paris')
ignore la valeur émise par l'Observable
of(city)
dans ce cas ; on obtient alors un Observable
qui "complete" sans émettre aucune valeur.
La fonction async
ne détecte donc aucun traitement en attente et la "callback" du premier subscribe
n'est jamais appelée.
Ce problème pourrait être résolu en utilisation la fonction done
mais il est dommage d'attendre une seconde de délai due au "pipe" delay(1000)
.
fakeAsync()
💪La fonction Angular fakeAsync
permet de contrôler l'"Event Loop" et le "Timer" 🎉.
Elle utilise également une "Zone" (Cf. Zone.JS) mais contrairement à la fonction async
, celle-ci n'attend pas la fin d'exécution des traitements asynchrones mais déclenche des erreurs à la fin de la "spec" si des traitements sont encore en attente.
La "spec" s'exécute plus rapidement car elle n'attend pas le délai d'une seconde mais elle produit rapidement l'erreur suivante :
tick()
& flush()
La fonction fakeAsync
est accompagnée des deux fonctions tick
et flush
qui permettent de contrôler la "Fake Event Loop" créée par la fonction fakeAsync
.
tick()
La fonction tick
permet de déclencher le prochain traitement en attente dans la "queue" de l'"Event Loop" :
tick(ms)
La fonction tick
permet également de simuler l'attente en lui donnant en paramètre le nombre de millisecondes à simuler.
flush()
Et finalement, la fonction flush
déclenche les (ou le) traitements en attente et retourne la valeur en millisecondes du temps d'attente simulé.
Grâce à la fonction fakeAsync
, on peut donc remédier aux limitations associées à la fonction async
ainsi :
Le test échoue alors car la "callback" n'a pas été appelée et temperature
vaut donc undefined
.
Les méthodes fakeAsync
, tick
et flush
sont généralement stables mais tout de même considérées comme expérimentales.
En effet, dans le dernier exemple, la fonction flush
ne fonctionne pas car il existe des incompatibilités avec certains Observable
s et opérateurs RxJS manipulant le timer (par manque de Monkey Patching ?), Cf. _https://github.com/angular/angular/issues/10127._
On pourrait recommander de déclarer toutes les "specs" avec la fonction fakeAsync
afin d'éviter tout effet de bord dû à des traitements asynchrones ignorés par le test.