Unit-Test Asynchrone
"Async Gotcha"
describe('new planet rules', () => {
it('should agree that 1 is equal to 2', () => {
setTimeout(() => {
expect(1).toEqual(2);
});
});
});WTF !? 🤔

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.
Solution n°1 : Fonction done 👎
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".
Solution n°2 : Promise et async / await ✌️
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.
Solution n°3 : Fonction async() 🤟
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).
Avantages
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.
Limitations
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).
Solution n°4 : Fonction fakeAsync() 💪
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()
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()
tick()La fonction tick permet de déclencher le prochain traitement en attente dans la "queue" de l'"Event Loop" :
tick(ms)
tick(ms)La fonction tick permet également de simuler l'attente en lui donnant en paramètre le nombre de millisecondes à simuler.
flush()
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 :
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 Observables et opérateurs RxJS manipulant le timer (par manque de Monkey Patching ?), Cf. _https://github.com/angular/angular/issues/10127._
Detection des effets de bord
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.
Mis à jour