Appearance
fakeAsync et tick : Tester le code asynchrone en Angular
Dans Angular, beaucoup de fonctionnalités reposent sur l'asynchrone : les Observables, les Promises, les timers... Tester ce code peut être complexe car il faut attendre que les opérations se terminent. fakeAsync
et tick
sont des outils Angular qui permettent de contrôler le temps dans vos tests pour les rendre plus rapides et prévisibles.
🕐 Analogie du quotidien
Imaginez que vous testez un minuteur de cuisine. Normalement, vous devriez attendre 3 minutes pour voir si l'alarme se déclenche. Mais avec fakeAsync
et tick
, c'est comme si vous aviez une télécommande magique qui vous permet d'accélérer le temps : vous pouvez faire défiler 3 minutes en quelques millisecondes !
🎯 Pourquoi utiliser fakeAsync et tick ?
En Angular, vous rencontrez souvent du code asynchrone :
- Observables avec des délais
- setTimeout et setInterval
- Promises avec des délais
- async pipe dans les templates
Sans fakeAsync
, vos tests devraient attendre le temps réel, ce qui les rend lents et peu fiables.
🔧 Les bases de fakeAsync et tick
fakeAsync : Le contrôleur de temps
fakeAsync
encapsule votre test dans un environnement où le temps est contrôlé. Angular remplace l'horloge système par une horloge virtuelle.
tick : L'accélérateur de temps
tick()
fait avancer l'horloge virtuelle d'un nombre de millisecondes spécifié.
📝 Exemple simple avec setTimeout
Voici comment tester un code qui utilise setTimeout
:
typescript
import { fakeAsync, tick } from '@angular/core/testing';
it('devrait mettre à jour la valeur après 1 seconde', fakeAsync(() => {
let valeur = 0;
// Simule une opération qui prend 1 seconde
setTimeout(() => {
valeur = 42;
}, 1000);
// Au début, la valeur n'a pas encore changé
expect(valeur).toBe(0);
// On avance le temps de 1000ms instantanément
tick(1000);
// Maintenant la valeur a été mise à jour
expect(valeur).toBe(42);
}));
🚀 Exemple avec un Observable
Testons un service qui retourne des données avec un délai :
typescript
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { fakeAsync, tick } from '@angular/core/testing';
it('devrait retourner les utilisateurs après 500ms', fakeAsync(() => {
let resultat: User[] | undefined;
// Simule un appel API avec délai
of([
{ id: 1, name: 'Alice', username: 'alice', email: '[email protected]' },
{ id: 2, name: 'Bob', username: 'bob', email: '[email protected]' }
]).pipe(delay(500)).subscribe(users => resultat = users);
// Au début, pas encore de résultat
expect(resultat).toBeUndefined();
// On avance le temps de 500ms
tick(500);
// Maintenant on a les utilisateurs
expect(resultat).toHaveLength(2);
expect(resultat![0].name).toBe('Alice');
}));
🧪 Exemple complet avec un composant
Créons un composant qui charge des utilisateurs avec un délai :
typescript
import { Component, signal } from '@angular/core';
import { User } from './user.interface';
@Component({
selector: 'app-user-list',
template: `
@if (loading()) {
<p>Chargement...</p>
} @else {
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
}
`
})
export class UserListComponent {
users = signal<User[]>([]);
loading = signal(true);
constructor() {
this.loadUsers();
}
// Simule le chargement des utilisateurs avec un délai
private loadUsers(): void {
setTimeout(() => {
this.users.set([
{ id: 1, name: 'Alice', username: 'alice', email: '[email protected]' },
{ id: 2, name: 'Bob', username: 'bob', email: '[email protected]' }
]);
this.loading.set(false);
}, 1000);
}
}
Testons ce composant :
typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { fakeAsync, tick } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [UserListComponent]
});
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('devrait afficher les utilisateurs après le chargement', fakeAsync(() => {
// Au début, on est en mode chargement
expect(component.loading()).toBe(true);
expect(component.users()).toHaveLength(0);
// On avance le temps de 1000ms
tick(1000);
// Maintenant les utilisateurs sont chargés
expect(component.loading()).toBe(false);
expect(component.users()).toHaveLength(2);
expect(component.users()[0].name).toBe('Alice');
}));
});
⚡ tick() sans paramètre
Vous pouvez utiliser tick()
sans paramètre pour avancer le temps jusqu'à ce que toutes les tâches en attente soient terminées :
typescript
it('devrait traiter toutes les tâches en attente', fakeAsync(() => {
let compteur = 0;
setTimeout(() => compteur++, 100);
setTimeout(() => compteur++, 200);
setTimeout(() => compteur++, 300);
// Avance le temps jusqu'à ce que toutes les tâches soient terminées
tick();
expect(compteur).toBe(3);
}));
🎯 Cas d'usage avancés
Tester des animations
typescript
it('devrait animer l\'apparition d\'un élément', fakeAsync(() => {
const element = document.createElement('div');
element.style.transition = 'opacity 0.5s';
element.style.opacity = '0';
// Déclenche l'animation
element.style.opacity = '1';
// Avance le temps de la durée de l'animation
tick(500);
// Vérifie que l'animation est terminée
expect(element.style.opacity).toBe('1');
}));
Tester des retry avec backoff
typescript
it('devrait retry avec un délai exponentiel', fakeAsync(() => {
let tentatives = 0;
const maxTentatives = 3;
function essayer(): void {
tentatives++;
if (tentatives < maxTentatives) {
setTimeout(() => essayer(), Math.pow(2, tentatives) * 100);
}
}
essayer();
// Avance le temps pour toutes les tentatives
tick(100 + 200 + 400); // 700ms au total
expect(tentatives).toBe(3);
}));
⚠️ Bonnes pratiques
Règles importantes
- Utilisez
fakeAsync
uniquement pour tester du code asynchrone - N'oubliez pas d'appeler
tick()
pour avancer le temps - Utilisez
tick()
sans paramètre pour traiter toutes les tâches en attente - Testez les cas d'erreur avec des délais
Attention
tick()
ne fonctionne que dans un contextefakeAsync
- Ne mélangez pas
fakeAsync
avecasync/await
dans le même test - Certaines APIs natives (comme
fetch
) ne sont pas interceptées parfakeAsync
🔍 Dépannage courant
Erreur : "tick is called outside of fakeAsync"
typescript
// ❌ Incorrect
it('test', () => {
tick(1000); // Erreur !
});
// ✅ Correct
it('test', fakeAsync(() => {
tick(1000); // OK !
}));
Tester des Promises
typescript
it('devrait résoudre une Promise avec délai', fakeAsync(() => {
let resultat: string | undefined;
new Promise<string>(resolve => {
setTimeout(() => resolve('Succès'), 1000);
}).then(valeur => resultat = valeur);
expect(resultat).toBeUndefined();
tick(1000);
expect(resultat).toBe('Succès');
}));