Gestion du temps dans les tests : patterns fiables pour l'asynchrone et les délais
Le temps est le tueur silencieux de la fiabilité des tests. Un test qui passe en local échoue en CI. Une suite qui s’exécutait en quelques secondes prend maintenant plusieurs minutes. Un composant qui fonctionne parfaitement affiche des échecs fantômes parce que les timers et les promesses ne coopèrent pas.
Cet article couvre les patterns modernes de gestion du temps dans les tests asynchrones et les bonnes pratiques des fake timers pour Jest, Vitest, React Testing Library, Playwright et Cypress—en se concentrant sur ce qui fonctionne réellement en 2025.
Points clés à retenir
- Utilisez les API de timers asynchrones (
advanceTimersByTimeAsync) plutôt que les variantes synchrones pour éviter les blocages entre promesses et timers - Configurez l’avancement automatique des timers lorsque vous combinez les fake timers avec les utilitaires asynchrones de Testing Library
- Nettoyez toujours les fake timers dans
afterEachpour éviter la pollution des tests et les échecs en cascade - Dans les tests E2E, attendez des conditions spécifiques plutôt que d’utiliser des délais arbitraires comme
waitForTimeoutoucy.wait(ms)
Pourquoi le temps casse les tests
Les tests échouent autour du temps pour trois raisons : les délais réels ralentissent l’exécution, les variations de timing causent de l’instabilité, et les fake timers entrent en conflit avec les promesses.
La boucle d’événements JavaScript traite les macrotâches (setTimeout, setInterval) et les microtâches (callbacks de Promise) dans un ordre spécifique. Les fake timers interceptent les macrotâches mais ne gèrent pas automatiquement les microtâches. Ce décalage cause la plupart des échecs de tests liés aux timers.
Timers asynchrones Jest : l’approche moderne
L’implémentation actuelle des fake timers de Jest utilise @sinonjs/fake-timers en interne. La clé pour des tests asynchrones fiables est de comprendre quelles API utiliser ensemble.
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('debounced search triggers after delay', async () => {
render(<SearchInput />);
await userEvent.type(screen.getByRole('textbox'), 'query');
await jest.advanceTimersByTimeAsync(300);
expect(mockSearch).toHaveBeenCalledWith('query');
});
Le détail critique : utilisez advanceTimersByTimeAsync au lieu de advanceTimersByTime. La variante asynchrone vide les microtâches entre les exécutions de timers, évitant le blocage promesse/timer qui cause des tests qui se figent.
Pour les animations, Jest fournit jest.advanceTimersToNextFrame() pour parcourir les callbacks requestAnimationFrame sans attendre le timing réel des frames.
Fake timers Vitest : intégration Sinon
Vitest partage la même base @sinonjs/fake-timers, ce qui fait que les fake timers Vitest se comportent de manière similaire à Jest avec des différences mineures d’API.
import { vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test('toast disappears after timeout', async () => {
render(<Toast message="Saved" />);
await vi.advanceTimersByTimeAsync(3000);
expect(screen.queryByText('Saved')).not.toBeInTheDocument();
});
Les deux frameworks supportent l’avancement automatique des timers, qui fait avancer le temps automatiquement lorsque la boucle d’événements serait autrement inactive. Cela élimine la gestion manuelle des ticks pour la plupart des tests.
Discover how at OpenReplay.com.
Coordination avec React Testing Library
L’interaction entre les fake timers et le waitFor de Testing Library cause fréquemment de la confusion. Lorsque les fake timers sont actifs, waitFor effectue des sondages mais le temps n’avance pas—créant des boucles infinies.
La solution : configurer les fake timers pour qu’ils avancent automatiquement pendant les attentes asynchrones.
jest.useFakeTimers({ advanceTimers: true });
Cela indique à Jest de faire avancer les timers lorsque des promesses sont en attente, permettant à waitFor et aux requêtes findBy de fonctionner naturellement avec les fake timers.
Pour user-event, assurez-vous d’utiliser l’API actuelle. La bibliothèque gère son propre timing en interne et se coordonne avec les fake timers lorsqu’ils sont correctement configurés.
Contrôle du temps E2E : Playwright et Cypress
L’API page.clock de Playwright fournit un contrôle précis du temps dans les contextes de navigateur :
await page.clock.install({ time: new Date('2025-01-15') });
await page.clock.fastForward(5000);
Cypress offre cy.clock() et cy.tick() pour un contrôle similaire. Ces deux approches sont préférables à waitForTimeout ou cy.wait(ms), qui introduisent des délais réels et de l’instabilité.
Pièges courants qui causent de l’instabilité
Fuites de timers entre les tests : Restaurez toujours les timers réels dans afterEach. Les fake timers qui fuient causent des échecs en cascade dans les tests suivants.
Conflits avec des bibliothèques tierces : Les bibliothèques comme les frameworks d’animation ou les utilitaires de polling planifient leurs propres timers. Ceux-ci peuvent ne pas respecter votre installation de fake timers, causant un comportement inattendu.
Ordre microtâches/macrotâches : Le code qui mélange setTimeout avec Promise.resolve() se comporte différemment sous fake timers. Utilisez les API de timers asynchrones de manière cohérente.
Considérations sur l’API Temporal : Les bases de code plus récentes utilisant Temporal au lieu de Date nécessitent des stratégies de mocking séparées. Abstraire votre source de temps rend les tests indépendants du framework.
Conclusion
Privilégiez les API de timers asynchrones (advanceTimersByTimeAsync) plutôt que les variantes synchrones. Configurez l’avancement automatique lors de l’utilisation des utilitaires asynchrones de Testing Library. Évitez les pauses arbitraires et waitForTimeout dans les tests E2E—attendez plutôt des conditions spécifiques. Nettoyez toujours les fake timers pour éviter la pollution des tests.
L’objectif est d’avoir des tests qui s’exécutent rapidement, passent de manière cohérente et n’encodent pas les détails d’implémentation du timing. Les fake timers avec avancement automatique vous amènent déjà très loin. Pour tout le reste, une coordination asynchrone explicite bat les délais arbitraires à chaque fois.
FAQ
Les tests se figent lorsque les fake timers bloquent les macrotâches pendant que les promesses attendent la résolution des microtâches. Utilisez les méthodes de timers asynchrones comme advanceTimersByTimeAsync au lieu de advanceTimersByTime. Les variantes asynchrones vident les microtâches entre les exécutions de timers, brisant le blocage entre promesses et timers.
Configurez Jest avec l'option advanceTimers activée en appelant jest.useFakeTimers({ advanceTimers: true }). Cela indique à Jest de faire avancer automatiquement le temps lorsque des promesses sont en attente, permettant à waitFor et aux requêtes findBy de sonder correctement sans créer de boucles infinies.
Utilisez les fake timers via page.clock de Playwright ou cy.clock et cy.tick de Cypress. Les délais réels avec waitForTimeout ou cy.wait introduisent de l'instabilité car la vitesse d'exécution varie selon les environnements. Attendez plutôt des conditions UI spécifiques ou utilisez l'avancement contrôlé du temps.
Les environnements CI ont des ressources CPU et mémoire variables, causant un comportement incohérent des tests dépendants du timing. Remplacez les délais réels par des fake timers, utilisez des assertions asynchrones qui attendent des conditions plutôt que des durées fixes, et assurez un nettoyage approprié des timers entre les tests pour éviter la pollution.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.