テストにおける時間の扱い方:非同期処理と遅延のための信頼性の高いパターン
時間はテストの信頼性を密かに破壊する要因です。ローカルで成功したテストがCIで失敗する。数秒で完了していたテストスイートが今では数分かかる。完璧に動作するコンポーネントが、タイマーとPromiseが協調しないために幻のような失敗を示す。
本記事では、Jest、Vitest、React Testing Library、Playwright、Cypressにおける最新の非同期テストタイミングパターンとフェイクタイマーのベストプラクティスを、2025年に実際に機能するものに焦点を当てて解説します。
重要なポイント
- Promise/タイマーのデッドロックを防ぐため、同期的なバリアントではなく非同期タイマーAPI(
advanceTimersByTimeAsync)を使用する - Testing Libraryの非同期ユーティリティとフェイクタイマーを組み合わせる場合は、自動進行タイマーを設定する
- テストの汚染と連鎖的な失敗を防ぐため、必ず
afterEachでフェイクタイマーをクリーンアップする - E2Eテストでは、
waitForTimeoutやcy.wait(ms)のような任意の遅延ではなく、特定の条件を待つ
なぜ時間がテストを壊すのか
テストが時間に関連して失敗する理由は3つあります:実際の遅延が実行を遅くする、タイミングのばらつきが不安定性を引き起こす、そしてフェイクタイマーがPromiseと競合する。
JavaScriptのイベントループは、マクロタスク(setTimeout、setInterval)とマイクロタスク(Promiseのコールバック)を特定の順序で処理します。フェイクタイマーはマクロタスクをインターセプトしますが、マイクロタスクを自動的に処理しません。この不一致が、タイマー関連のテスト失敗のほとんどを引き起こします。
Jest非同期タイマー:モダンなアプローチ
Jestの現在のフェイクタイマー実装は、内部で@sinonjs/fake-timersを使用しています。信頼性の高い非同期テストの鍵は、どのAPIを一緒に使用するかを理解することです。
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');
});
重要な詳細:advanceTimersByTimeの代わりにadvanceTimersByTimeAsyncを使用します。非同期バリアントはタイマー実行の間にマイクロタスクをフラッシュし、テストのハングを引き起こすPromise/タイマーのデッドロックを防ぎます。
アニメーションの場合、Jestは実際のフレームタイミングを待たずにrequestAnimationFrameコールバックをステップ実行するためのjest.advanceTimersToNextFrame()を提供しています。
Vitestフェイクタイマー:Sinon統合
Vitestは同じ@sinonjs/fake-timers基盤を共有しているため、VitestフェイクタイマーはわずかなAPI差異を除いてJestと同様に動作します。
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();
});
両フレームワークとも自動進行タイマーをサポートしており、イベントループがアイドル状態になるときに自動的に時間を進めます。これにより、ほとんどのテストで手動のtick管理が不要になります。
Discover how at OpenReplay.com.
React Testing Libraryとの連携
フェイクタイマーとTesting LibraryのwaitForの相互作用は、頻繁に混乱を引き起こします。フェイクタイマーが有効な場合、waitForはポーリングしますが時間が進まず、無限ループが発生します。
解決策:非同期待機中に自動的に進行するようフェイクタイマーを設定します。
jest.useFakeTimers({ advanceTimers: true });
これにより、Promiseが保留中のときにタイマーを進めるようJestに指示し、waitForとfindByクエリがフェイクタイマーと自然に連携できるようになります。
user-eventの場合、現在のAPIを使用していることを確認してください。このライブラリは内部で独自のタイミングを処理し、正しく設定されている場合はフェイクタイマーと連携します。
E2E時間制御:PlaywrightとCypress
Playwrightのpage.clock APIは、ブラウザコンテキストで正確な時間制御を提供します:
await page.clock.install({ time: new Date('2025-01-15') });
await page.clock.fastForward(5000);
Cypressは同様の制御のためにcy.clock()とcy.tick()を提供しています。どちらのアプローチも、実際の遅延と不安定性をもたらすwaitForTimeoutやcy.wait(ms)よりも望ましいです。
不安定性を引き起こす一般的な落とし穴
テスト間のタイマーリーク:afterEachで必ず実際のタイマーを復元してください。リークしたフェイクタイマーは、後続のテストで連鎖的な失敗を引き起こします。
サードパーティライブラリとの競合:アニメーションフレームワークやポーリングユーティリティなどのライブラリは、独自のタイマーをスケジュールします。これらはフェイクタイマーのインストールを尊重しない可能性があり、予期しない動作を引き起こします。
マイクロタスク/マクロタスクの順序:setTimeoutとPromise.resolve()を混在させるコードは、フェイクタイマー下では異なる動作をします。非同期タイマーAPIを一貫して使用してください。
Temporal APIの考慮事項:Dateの代わりにTemporalを使用する新しいコードベースには、別のモック戦略が必要です。テストをフレームワークに依存しないものにするため、時間ソースを抽象化してください。
まとめ
同期バリアントよりも非同期タイマーAPI(advanceTimersByTimeAsync)を優先してください。Testing Libraryの非同期ユーティリティを使用する場合は、自動進行を設定してください。E2Eテストでは任意のスリープやwaitForTimeoutを避け、代わりに特定の条件を待ってください。テストの汚染を防ぐため、必ずフェイクタイマーをクリーンアップしてください。
目標は、高速に実行され、一貫して成功し、タイミング実装の詳細をエンコードしないテストです。自動進行フェイクタイマーがその大部分を実現します。それ以外のすべてについては、明示的な非同期連携が任意の遅延に勝ります。
よくある質問
フェイクタイマーがマクロタスクをブロックし、Promiseがマイクロタスクの解決を待つときにテストがハングします。advanceTimersByTimeの代わりにadvanceTimersByTimeAsyncのような非同期タイマーメソッドを使用してください。非同期バリアントはタイマー実行の間にマイクロタスクをフラッシュし、Promiseとタイマーのデッドロックを解消します。
jest.useFakeTimers({ advanceTimers: true })を呼び出してadvanceTimersオプションを有効にしてJestを設定します。これにより、Promiseが保留中のときに自動的に時間を進めるようJestに指示し、waitForとfindByクエリが無限ループを作成せずに正しくポーリングできるようになります。
Playwrightのpage.clockまたはCypressのcy.clockとcy.tickを介してフェイクタイマーを使用してください。waitForTimeoutやcy.waitを使用した実際の遅延は、実行速度が環境によって異なるため不安定性をもたらします。代わりに、特定のUI条件を待つか、制御された時間進行を使用してください。
CI環境にはCPUとメモリリソースの変動があり、タイミング依存のテストが一貫性のない動作をします。実際の遅延をフェイクタイマーに置き換え、固定期間ではなく条件を待つ非同期アサーションを使用し、テスト間の汚染を防ぐために適切なタイマークリーンアップを確保してください。
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.