Le Guide Angular | Marmicode
  • Le Guide Angular par Marmicode
  • Pourquoi Angular ?
  • ECMAScript 6+
    • Un Peu d'Histoire
    • Propriétés du Langage
    • "Single-Threaded" donc Asynchrone
    • Event Loop
    • Classes
    • Hoisting is Dead: var vs. let vs. const
    • this & "binding"
    • Arrow Functions
    • Template Strings
    • Syntactic Sugar
      • Spread
      • Destructuring
      • Rest
      • Object Literal Property Value Shorthand
    • Named Parameters
    • Compatibilité
  • TypeScript
    • Pourquoi TypeScript ?
    • De l'ECMAScript au TypeScript
    • Visibilité des Propriétés
    • Typing des Propriétés
    • Types
    • Interfaces
    • Inference
    • Duck Typing
    • Duck Typing Patterns
      • Compatibilité de Librairies
      • Entity Constructor
    • Décorateurs
      • Décorateurs de Propriété
      • Décorateurs de Classe
      • Décorateurs de Méthode & Paramètres
    • Quelques Liens
  • Tools
    • Clavier mécanique
    • Git
    • Command Line
    • NodeJS
    • NPM
    • Yarn
      • Pourquoi Yarn ?
      • Définition et Installation des Dépendances
      • Scripts
      • Mise à Jour et Automatisation
    • Chrome
    • IntelliJ / WebStorm / VSCode
      • Raccourcis clavier IntelliJ / WebStorm
    • Floobits
    • Angular CLI
    • StackBlitz
    • Compodoc
  • Angular
    • Bootstrap
    • Composants
      • Root Component
      • Template Interpolation
      • Property Binding
      • Class & Style Binding
      • Event Binding
      • *ngIf
      • *ngFor
      • L'approche MVC
      • Création de Composants
      • Exemple
    • Container vs. Presentational Components
    • Interaction entre Composants
      • Input
      • Output
      • Exemple
    • Change Detection
      • Les Approches Possibles
      • Fonctionnement de la Change Detection
      • Optimisation de la Change Detection
      • Immutabilité
      • Quelques Liens
    • Project Structure & Modules
      • Entry Point
      • Définition d'un Module
      • Root Module
      • Feature Module
      • Shared Module
      • Exemple
    • Dependency Injection
      • Qu'est-ce que la "Dependency Injection" ?
      • Injection d'un Service Angular
      • Services & Providers
      • Portée des Services
      • Tree-Shakable Services
      • Class vs Injection Token
      • Exemple
    • Callback Hell vs. Promise vs. Async / Await
      • Callback Hell
      • Promise
      • Async / Await
    • Observables
      • Reactive Programming
      • Promise vs Observable
      • Subscribe
      • Unsubscribe ⚠️
      • Création d'un Observable
      • Opérateurs
        • Définition d'un Opérateur
        • Lettable Operators vs Legacy Methods
        • map
        • filter
        • mergeMap & switchMap
        • shareReplay
        • buffer
        • debounceTime
        • distinctUntilChanged
        • retry
      • Quelques Liens
      • Talks
    • Http
      • Pourquoi HttpClient ?
      • Utilisation de HttpClient
      • Utilisation dans un Service
      • Gestion de la Subscription ⚠️
    • State Management
      • Quelques Liens
      • Talks
    • GraphQL
    • Formulaires
      • Template-driven Forms 🤢
      • Reactive Forms 👍
        • Avantages des "Reactive Forms"
        • La boite à outils des "Reactive Forms"
        • Validation
        • Observation des Changements
    • Directives
      • Attribute Directive
      • Structural Directive
    • Pipes
    • Routing
      • Mise en Place du Routing
      • Lazy Loading
      • Project Structure
      • Route Guards
    • Testing
      • Unit-Testing
        • 📺Introduction au Test-Driven Development
        • Jasmine
        • Unit-Test Synchrone
        • Test-Driven Development
        • Unit-Test Asynchrone
        • TestBed
        • Unit-Test d'un Service
        • Unit-Test d'un Composant
        • Unit-Test et Spies
        • Unit-Test et HttpClient
      • End-to-End
      • Talks
    • Sécurité
      • Quelques Liens
    • Animation
    • Internationalisation
    • Quelques Liens
  • Cookbook
    • Authentification et Autorisation
    • Remplacement Dynamique de Composants
    • Lazy Loading without Router
    • Project Structure
    • SCAM Modules
    • Setup a Mock ReSTful API
  • Autres Ressources
  • Stay Tuned
    • 🎁-20% sur nos workshops avec le code GUIDEANGULAR
    • 🐦Suivez-moi !
    • 📺Cours Vidéo
    • 📬Newsletter
    • 📝Blog
  • Nos Services
    • Formation Angular
    • Atelier Unit-Testing Angular
    • Atelier Architecture Angular
    • Consultation à Distance & Code Review
  • Nos Guides
    • Guide Agile
    • Guide API ReST
    • Guide NodeJS
Propulsé par GitBook
Sur cette page
  • "Async Gotcha"
  • WTF !? 🤔
  • Solution n°1 : Fonction done 👎
  • Solution n°2 : Promise et async / await ✌️
  • Solution n°3 : Fonction async() 🤟
  • Avantages
  • Limitations
  • Solution n°4 : Fonction fakeAsync() 💪
  • tick() & flush()
  1. Angular
  2. Testing
  3. Unit-Testing

Unit-Test Asynchrone

PrécédentTest-Driven DevelopmentSuivantTestBed

Dernière mise à jour il y a 5 ans

"Async Gotcha"

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

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

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

    });

});

WTF !? 🤔

Solution n°1 : Fonction done 👎

Pour indiquer à Jasmine que la "spec" est asynchrone, il faut ajouter le paramètre done à la fonction de "spec".

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

    it('should agree that 1 is equal to 2', (done) => {
        // Nothing 😪
    });

});

On obtient alors l'erreur suivante :

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

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".

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

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

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

    });

});

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".

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.

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

    it('should not affect arithmetic rules', () => {

        const promise1 = new Promise(resolve => {

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

        });

        const promise2 = new Promise(resolve => {

            setTimeout(() => {
                expect(1).not.toEqual(3);
                resolve();
            });

        });

        return Promise.all([promise1, promise2]);

    });

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

    it('should not affect arithmetic rules', async () => {

        await new Promise(resolve => {

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

        });

        await new Promise(resolve => {

            setTimeout(() => {
                expect(1).not.toEqual(3);
                resolve();
            });

        });

    });

});

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.

import { async } from '@angular/core/testing';

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

    it('should not affect arithmetic rules', async(() => {

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

    }));

});

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 :

import { Observable, timer } from 'rxjs';
import { filter, map, mapTo } from 'rxjs/operators';

class PickyWeatherStation {

    getTemperature(city): Observable<number> {

        return timer(1000)
            .pipe(
                mapTo(city),
                filter(_city => _city !== 'Paris'),
                map(_city => 100 / _city.length)
            );

    }

}
describe('PickyWeatherStation', () => {

    let weatherStation: PickyWeatherStation;

    beforeEach(() => {
        weatherStation = new PickyWeatherStation();
    });

    it('should give temperature', async(() => {

        weatherStation.getTemperature('Paris')
            .subscribe(temperature => {
                expect(temperature).toEqual(-10);
            });

    }));

});

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() 💪

La fonction Angular fakeAsync permet de contrôler l'"Event Loop" et le "Timer" 🎉.

import { fakeAsync } from '@angular/core/testing';

...

    it('should give temperature', fakeAsync(() => {

        weatherStation.getTemperature('Paris')
            .subscribe(temperature => {
                expect(temperature).toEqual(-10);
            });

    }));

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 :

1 periodic timer(s) still in the queue.

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" :

    it('should trigger next tick', fakeAsync(() => {

        let value;

        setTimeout(() => value = 'VALUE');

        tick();

        expect(value).toEqual('VALUE');

    }));

tick(ms)

La fonction tick permet également de simuler l'attente en lui donnant en paramètre le nombre de millisecondes à simuler.

it('should control time', fakeAsync(() => {

    let value;

    setTimeout(() => value = 'VALUE', 1000);

    tick(999);

    expect(value).toEqual(undefined);

    tick(1000);

    expect(value).toEqual('VALUE');

}));

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é.

it('should trigger next tick', fakeAsync(() => {

    let valueList = [];

    setTimeout(() => valueList = [...valueList, 'WISH'], 1000);
    setTimeout(() => valueList = [...valueList, 'TACK'], 2000);

    expect(valueList).toEqual([]);

    const duration = flush();

    expect(valueList.join('')).toEqual('WISHTACK');

    expect(duration).toEqual(2000);

}));

Grâce à la fonction fakeAsync, on peut donc remédier aux limitations associées à la fonction async ainsi :

it('should give temperature', fakeAsync(() => {

    let temperature;

    weatherStation.getTemperature('Paris')
        .subscribe(_temperature => temperature = _temperature);

    tick(1000);

    expect(temperature).toBe(20);

}));

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.

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.

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 n'arrive à l'associer à aucun test.

Elle finit rapidement en et "Race Conditions".

Plus simple et plus sexy, il est possible d'utiliser .

En créant une "Zone" (Cf. ) 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).

Elle utilise également une "Zone" (Cf. ) 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.

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. _._

Jasmine
Callback Hell
async / await
Zone.JS
Zone.JS
https://github.com/angular/angular/issues/10127
Success !!!