Back

テストにおける時間の扱い方:非同期処理と遅延のための信頼性の高いパターン

テストにおける時間の扱い方:非同期処理と遅延のための信頼性の高いパターン

時間はテストの信頼性を密かに破壊する要因です。ローカルで成功したテストがCIで失敗する。数秒で完了していたテストスイートが今では数分かかる。完璧に動作するコンポーネントが、タイマーとPromiseが協調しないために幻のような失敗を示す。

本記事では、Jest、Vitest、React Testing Library、Playwright、Cypressにおける最新の非同期テストタイミングパターンフェイクタイマーのベストプラクティスを、2025年に実際に機能するものに焦点を当てて解説します。

重要なポイント

  • Promise/タイマーのデッドロックを防ぐため、同期的なバリアントではなく非同期タイマーAPI(advanceTimersByTimeAsync)を使用する
  • Testing Libraryの非同期ユーティリティとフェイクタイマーを組み合わせる場合は、自動進行タイマーを設定する
  • テストの汚染と連鎖的な失敗を防ぐため、必ずafterEachでフェイクタイマーをクリーンアップする
  • E2Eテストでは、waitForTimeoutcy.wait(ms)のような任意の遅延ではなく、特定の条件を待つ

なぜ時間がテストを壊すのか

テストが時間に関連して失敗する理由は3つあります:実際の遅延が実行を遅くする、タイミングのばらつきが不安定性を引き起こす、そしてフェイクタイマーがPromiseと競合する。

JavaScriptのイベントループは、マクロタスク(setTimeoutsetInterval)とマイクロタスク(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管理が不要になります。

React Testing Libraryとの連携

フェイクタイマーとTesting LibrarywaitForの相互作用は、頻繁に混乱を引き起こします。フェイクタイマーが有効な場合、waitForはポーリングしますが時間が進まず、無限ループが発生します。

解決策:非同期待機中に自動的に進行するようフェイクタイマーを設定します。

jest.useFakeTimers({ advanceTimers: true });

これにより、Promiseが保留中のときにタイマーを進めるようJestに指示し、waitForfindByクエリがフェイクタイマーと自然に連携できるようになります。

user-eventの場合、現在のAPIを使用していることを確認してください。このライブラリは内部で独自のタイミングを処理し、正しく設定されている場合はフェイクタイマーと連携します。

E2E時間制御:PlaywrightとCypress

Playwrightpage.clock APIは、ブラウザコンテキストで正確な時間制御を提供します:

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

Cypressは同様の制御のためにcy.clock()cy.tick()を提供しています。どちらのアプローチも、実際の遅延と不安定性をもたらすwaitForTimeoutcy.wait(ms)よりも望ましいです。

不安定性を引き起こす一般的な落とし穴

テスト間のタイマーリーク:afterEachで必ず実際のタイマーを復元してください。リークしたフェイクタイマーは、後続のテストで連鎖的な失敗を引き起こします。

サードパーティライブラリとの競合:アニメーションフレームワークやポーリングユーティリティなどのライブラリは、独自のタイマーをスケジュールします。これらはフェイクタイマーのインストールを尊重しない可能性があり、予期しない動作を引き起こします。

マイクロタスク/マクロタスクの順序:setTimeoutPromise.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.

OpenReplay