Unit-Test Asynchrone

"Async Gotcha"

describe('new planet rules', () => {

    it('should agree that 1 is equal to 2', () => {

        setTimeout(() => {
            expect(1).toEqual(2);
        });

    });

});

WTF !? 🤔

Success !!!

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 👎

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 :

La fonction done doit être appelée explicitement à la fin de la "spec".

Cette fois-ci, la "spec" échoue rapidement à cause de l'assertion.

Solution n°2 : 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() 🤟

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.

Solution n°4 : Fonction 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.

Detection des effets de bord

Mis à jour