## Vitest fake timersとwaitForの相性問題とは

React コンポーネントのテストで vi.useFakeTimers() を使用した際、waitFor との組み合わせで問題が発生するケースについて学びました。

正直なところ、これらの問題点をすべて理解するまでにかなりの時間を要しましたが、解決できたおかでテストの安定性が大幅に向上しました。

本記事では、実際の開発現場で遭遇した3つの典型的なトラブルシューティング事例と、それぞれの具体的な解決策を解説します。

### 検索でこの記事にたどり着く方へ

この記事にたどり着いた方は、おそらく以下のような状況で躓いているのではないでしょうか。

- Vitestでテストを実行したら waitFor がタイムアウトしてしまう
- fake timersを使ったらテストが不安定になった
- モック関数の設定方法がよく分からない

まさに私が実際に遭遇した問題と同じ状況です。

この記事では、これらの問題に対する具体的な解決策と正しい実装方法をコード例と共に説明します。

### こんな人におすすめ

- Vitest と React Testing Library を組み合わせてテストを書いている方
- waitFor がタイムアウトする問題で困っている開発者
- fake timers を利用しているがテストが不安定になっている方
- モック関数やスプレッド演算子の使い方で躓いているエンジニア
- TypeScript で React アプリケーションのテストをしている方


## fake timers使用時によくある3つの問題

### 問題1: モック関数の設定ミス

mockFetch(createSuccessResponse(session)) のように直接呼び出すと、モック関数を呼び出しているだけで設定していないことになります。

これは非常によくある間違いで、一見正しく動作しているように見えるため、発見が遅れることがあります。

``typescript
// ❌ 間違い - モック関数を呼び出しているだけ
mockFetch(createSuccessResponse(session));

// ✅ 正解 - モックの戻り値を設定
mockFetch.mockResolvedValueOnce(createSuccessResponse(session));
`

**ポイント**: モック関数自体を呼び出すのではなく、
mockResolvedValueOncemockReturnValue などのメソッドを使って戻り値を設定する必要があります。

### 問題2: waitForがタイムアウトする原因

vi.useFakeTimers() を使用している状態で waitFor を使うと、waitFor 内部の setTimeout が fake timers の影響を受けてタイムアウトします。

これが本記事の主題である最も厄介な問題です。

この問題の仕組みをシーケンス図で説明します:

`mermaid
sequenceDiagram
participant T as テストコード
participant V as Vitest<br/>(fake timers)
participant W as waitFor
participant S as setTimeout

T->>V: vi.useFakeTimers()実行
activate V
Note over V: ⚠️ fake timers有効化

T->>W: waitFor実行開始
activate W
W->>W: 条件チェック

alt 条件を満たさない
W->>S: setTimeout(リトライ)
activate S
Note right of S: 通常は時間経過で次へ
S->>V: 時間経過を待機
V--xS: ⚠️ fake timersにより<br/>時間が進まない
deactivate S
Note right of S: タイムアウトエラー発生
end

deactivate W
deactivate V
`

**ポイント**: fake timersが有効な状態では、
waitFor内部のsetTimeoutによる時間経過が発生せず、永遠にリトライを続けるか、タイムアウトしてしまいます。

`typescript
// ❌ タイムアウトする可能性がある
vi.useFakeTimers();
render(<Component />);
await waitFor(() => {
expect(screen.getByTestId('user').textContent).toBe('testuser');
});
`

**原因**:
waitFor は内部的に setTimeout を使ってリトライ処理を行っていますが、fake timers が有効になっているとこの時間が進まず、結果としてタイムアウトします。

### 問題3: スプレッド演算子によるデータ消失

ネストしたオブジェクトの一部を変更しようとして、スプレッド演算子を使うと期待通りに動かないケースがあります。

この問題の仕組みを図で説明します:

`mermaid
graph LR
A[元のsessionオブジェクト] --> B[userプロパティを上書き?] B -->|スプレッド演算子| C[❌ 浅いマージ<br/>user全体が置換] B -->|createMockUserヘルパー| D[✅ 正しいマージ<br/>他プロパティ保持]
C --> E[id, nameなどが消失] D --> F[scopesのみ変更]
style C fill:#ffcccc
style E fill:#ff6666
style D fill:#d4edda
style F fill:#c3e6cb
`

**浅いマージの問題**: スプレッド演算子
{...obj}は1階層目のプロパティのみをマージし、ネストしたオブジェクトは丸ごと置換してしまいます。

`typescript
// ❌ user オブジェクト全体が置き換えられる(他のプロパティが消える)
createMockSession({
user: { scopes: ['images:generate'] }
});

// ✅ createMockUser を使って正しくマージ
createMockSession({
user: createMockUser({ scopes: ['images:generate'] })
});
`

**原因**: スプレッド演算子によるマージは「浅いマージ」であり、ネストしたオブジェクトは全体が置き換わってしまいます。


## 解決策と正しい実装方法

まずは、どの解決策を選ぶべきか判断するフローを示します:

`mermaid
flowchart TD
Start[fake timersで<br/>waitForがタイムアウトする] --> Q1{fake timersは<br/>本当に必要?}

Q1 -->|いいえ| S1[解決策1<br/>fake timersを使わない] Q1 -->|はい| Q2{時間を明示的に<br/>進められる?}

Q2 -->|はい| S2[解決策2<br/>advanceTimersByTimeAsync] Q2 -->|いいえ| Q3{waitForの前に<br/>タイマー操作完了?}

Q3 -->|はい| S3[解決策3<br/>useRealTimersで戻す] Q3 -->|いいえ| S1

S1 --> Result[✅ テスト安定化] S2 --> Result
S3 --> Result

style Start fill:#ffe6e6
style S1 fill:#d4edda
style S2 fill:#d4edda
style S3 fill:#d4edda
style Result fill:#c3e6cb
`

それぞれの解決策について詳しく見ていきます。

### fake timersを避ける方法

最もシンプルな解決策は、fake timers を使わないことです。

多くの場合、fake timers を必要としないテストケースでは、デフォルトのリアルタイマーを使用することで問題を回避できます。

`typescript
// ✅ fake timersを使わない
render(<Component />);
await waitFor(() => {
expect(screen.getByTestId('user').textContent).toBe('testuser');
});
`

### 時間を明示的に進める方法

どうしても fake timers が必要な場合は、
vi.advanceTimersByTimeAsync() を使って明示的に時間を進めます。

`typescript
// ✅ 時間を明示的に進める
vi.useFakeTimers();
render(<Component />);
await vi.advanceTimersByTimeAsync(1000); // 1秒進める
await waitFor(() => {
expect(screen.getByTestId('user').textContent).toBe('testuser');
});
`

### waitForの前にリアルタイマーに戻す方法

waitFor を使う直前に vi.useRealTimers() を呼ぶことで、一時的にリアルタイマーに戻すこともできます。

`typescript
// ✅ waitForの前にリアルタイマーに戻す
vi.useFakeTimers();
// ... fake timersが必要なテスト ...

vi.useRealTimers(); // リアルタイマーに戻す
await waitFor(() => {
expect(screen.getByTestId('user').textContent).toBe('testuser');
});
`

### 正しいモックデータの作成方法

ネストしたオブジェクトを変更する場合は、専用のヘルパー関数(
createMockUser など)を使って正しくマージします。

`typescript
// ✅ createMockUser ヘルパー関数を使う
createMockSession({
user: createMockUser({ scopes: ['images:generate'] })
});
`

この方法なら、
user オブジェクトの他のプロパティ(idname など)は保持されたまま、scopes のみを上書きできます。


## まとめ:テスト安定化のポイント

本記事で解説した3つの問題と解決策を振り返ります。

- モック関数の設定は
mockResolvedValueOnce などを使います
- fake timers と React Testing Library の
waitFor` は相性が悪いので注意が必要です
- オブジェクトのスプレッド演算子は浅いマージなので、ネストしたオブジェクトの一部だけを変更する場合は注意が必要です

実際にこれらの問題を解決できたおかで、テストの安定性が大幅に向上し、開発効率も改善されました。

同じ問題で躓いている方の参考になればと思います。

トラブルシューティングの際は、まず「fake timersを使っているか」「モック関数の設定は正しいか」「オブジェクトのマージは正しいか」の3点を確認してみてください。


## 参考リンク

- [Vitest Timers ドキュメント](https://vitest.dev/guide/mocking.html#timers)
- [Testing Library waitFor API](https://testing-library.com/docs/dom-testing-library/api-async/#waitfor)