Back

Umgang mit Zeit in Tests: Zuverlässige Muster für Async und Verzögerungen

Umgang mit Zeit in Tests: Zuverlässige Muster für Async und Verzögerungen

Zeit ist der stille Killer der Test-Zuverlässigkeit. Ein Test, der lokal erfolgreich ist, schlägt in der CI fehl. Eine Test-Suite, die in Sekunden durchlief, benötigt jetzt Minuten. Eine Komponente, die perfekt funktioniert, zeigt Phantom-Fehler, weil Timer und Promises nicht zusammenarbeiten.

Dieser Artikel behandelt moderne Async-Test-Timing-Muster und Best Practices für Fake Timer für Jest, Vitest, React Testing Library, Playwright und Cypress – mit Fokus auf das, was 2025 tatsächlich funktioniert.

Wichtigste Erkenntnisse

  • Verwenden Sie asynchrone Timer-APIs (advanceTimersByTimeAsync) anstelle von synchronen Varianten, um Promise/Timer-Deadlocks zu vermeiden
  • Konfigurieren Sie automatisch fortschreitende Timer, wenn Sie Fake Timer mit den Async-Utilities der Testing Library kombinieren
  • Räumen Sie Fake Timer immer in afterEach auf, um Test-Verschmutzung und kaskadierende Fehler zu verhindern
  • Warten Sie in E2E-Tests auf spezifische Bedingungen, anstatt beliebige Verzögerungen wie waitForTimeout oder cy.wait(ms) zu verwenden

Warum Zeit Tests zum Scheitern bringt

Tests scheitern aus drei Gründen im Zusammenhang mit Zeit: Echte Verzögerungen verlangsamen die Ausführung, Timing-Variationen verursachen Flakiness, und Fake Timer kollidieren mit Promises.

Die JavaScript Event Loop verarbeitet Macrotasks (setTimeout, setInterval) und Microtasks (Promise-Callbacks) in einer bestimmten Reihenfolge. Fake Timer fangen Macrotasks ab, behandeln aber nicht automatisch Microtasks. Diese Diskrepanz verursacht die meisten timer-bezogenen Testfehler.

Jest Async Timer: Der moderne Ansatz

Die aktuelle Fake-Timer-Implementierung von Jest verwendet unter der Haube @sinonjs/fake-timers. Der Schlüssel zu zuverlässigem Async-Testing ist das Verständnis, welche APIs zusammen verwendet werden sollten.

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');
});

Das entscheidende Detail: Verwenden Sie advanceTimersByTimeAsync anstelle von advanceTimersByTime. Die asynchrone Variante leert Microtasks zwischen Timer-Ausführungen und verhindert so den Promise/Timer-Deadlock, der zu hängenden Tests führt.

Für Animationen bietet Jest jest.advanceTimersToNextFrame(), um requestAnimationFrame-Callbacks schrittweise durchzugehen, ohne auf das tatsächliche Frame-Timing zu warten.

Vitest Fake Timer: Sinon-Integration

Vitest teilt dieselbe @sinonjs/fake-timers-Grundlage, wodurch sich Vitest Fake Timer ähnlich wie Jest verhalten, mit geringfügigen API-Unterschieden.

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();
});

Beide Frameworks unterstützen automatisch fortschreitende Timer, die die Zeit automatisch vorantreiben, wenn die Event Loop andernfalls im Leerlauf wäre. Dies eliminiert die manuelle Tick-Verwaltung für die meisten Tests.

Koordination mit React Testing Library

Die Interaktion zwischen Fake Timern und Testing Library’s waitFor sorgt häufig für Verwirrung. Wenn Fake Timer aktiv sind, pollt waitFor, aber die Zeit schreitet nicht voran – was zu Endlosschleifen führt.

Die Lösung: Konfigurieren Sie Fake Timer so, dass sie während asynchroner Wartezeiten automatisch fortschreiten.

jest.useFakeTimers({ advanceTimers: true });

Dies weist Jest an, Timer voranzutreiben, wenn Promises ausstehen, sodass waitFor und findBy-Queries natürlich mit Fake Timern funktionieren.

Für user-event stellen Sie sicher, dass Sie die aktuelle API verwenden. Die Bibliothek handhabt ihr eigenes Timing intern und koordiniert sich mit Fake Timern, wenn sie korrekt konfiguriert ist.

E2E-Zeitkontrolle: Playwright und Cypress

Die page.clock-API von Playwright bietet präzise Zeitkontrolle in Browser-Kontexten:

await page.clock.install({ time: new Date('2025-01-15') });
await page.clock.fastForward(5000);

Cypress bietet cy.clock() und cy.tick() für ähnliche Kontrolle. Beide Ansätze sind waitForTimeout oder cy.wait(ms) vorzuziehen, die echte Verzögerungen und Flakiness einführen.

Häufige Fallstricke, die Flakiness verursachen

Timer-Leaks zwischen Tests: Stellen Sie echte Timer immer in afterEach wieder her. Auslaufende Fake Timer verursachen kaskadierende Fehler in nachfolgenden Tests.

Konflikte mit Drittanbieter-Bibliotheken: Bibliotheken wie Animations-Frameworks oder Polling-Utilities planen ihre eigenen Timer. Diese respektieren möglicherweise Ihre Fake-Timer-Installation nicht, was zu unerwartetem Verhalten führt.

Microtask/Macrotask-Reihenfolge: Code, der setTimeout mit Promise.resolve() mischt, verhält sich unter Fake Timern anders. Verwenden Sie asynchrone Timer-APIs konsistent.

Temporal-API-Überlegungen: Neuere Codebasen, die Temporal anstelle von Date verwenden, benötigen separate Mocking-Strategien. Abstrahieren Sie Ihre Zeitquelle, um Tests framework-agnostisch zu machen.

Fazit

Bevorzugen Sie asynchrone Timer-APIs (advanceTimersByTimeAsync) gegenüber synchronen Varianten. Konfigurieren Sie automatisches Fortschreiten bei Verwendung von Testing Library Async-Utilities. Vermeiden Sie beliebige Sleeps und waitForTimeout in E2E-Tests – warten Sie stattdessen auf spezifische Bedingungen. Räumen Sie Fake Timer immer auf, um Test-Verschmutzung zu verhindern.

Das Ziel sind Tests, die schnell laufen, konsistent bestehen und keine Timing-Implementierungsdetails kodieren. Automatisch fortschreitende Fake Timer bringen Sie größtenteils dorthin. Für alles andere schlägt explizite asynchrone Koordination beliebige Verzögerungen jedes Mal.

FAQs

Tests hängen, wenn Fake Timer Macrotasks blockieren, während Promises darauf warten, dass Microtasks aufgelöst werden. Verwenden Sie asynchrone Timer-Methoden wie advanceTimersByTimeAsync anstelle von advanceTimersByTime. Die asynchronen Varianten leeren Microtasks zwischen Timer-Ausführungen und durchbrechen so den Deadlock zwischen Promises und Timern.

Konfigurieren Sie Jest mit aktivierter advanceTimers-Option, indem Sie jest.useFakeTimers({ advanceTimers: true }) aufrufen. Dies weist Jest an, die Zeit automatisch voranzutreiben, wenn Promises ausstehen, sodass waitFor und findBy-Queries korrekt pollen können, ohne Endlosschleifen zu erzeugen.

Verwenden Sie Fake Timer über Playwrights page.clock oder Cypress' cy.clock und cy.tick. Echte Verzögerungen mit waitForTimeout oder cy.wait führen zu Flakiness, da die Ausführungsgeschwindigkeit in verschiedenen Umgebungen variiert. Warten Sie stattdessen auf spezifische UI-Bedingungen oder verwenden Sie kontrolliertes Zeitfortschreiten.

CI-Umgebungen haben variable CPU- und Speicherressourcen, wodurch sich timing-abhängige Tests inkonsistent verhalten. Ersetzen Sie echte Verzögerungen durch Fake Timer, verwenden Sie asynchrone Assertions, die auf Bedingungen statt auf feste Dauern warten, und stellen Sie ordnungsgemäße Timer-Bereinigung zwischen Tests sicher, um Verschmutzung zu verhindern.

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.

OpenReplay