魔法のタイマーで「待つ」を操る!useEffect内のタイマー処理をJestでサクッとテストする方法
「Reactって、なんだか色々あって難しい…!」
そう感じていませんか?特に、コンポーネントが画面に表示されたり消えたりするタイミングで「何か処理を実行したい」と思ったときに使う `useEffect`。その中で、ちょっとした「待ち時間」を設けるために `setTimeout` や `setInterval` を使うことがありますよね。
でも、このタイマー処理、テストしようとすると「あれ?思った通りに動かないぞ?」って、ちょっと迷子になりがちなんです。
大丈夫、大丈夫!今日のブログでは、そんなタイマー処理のテストを、まるで魔法のように簡単にしてくれる `jest.useFakeTimers` という秘密兵器を、とことん優しく、そして分かりやすく解説していきます。
なんでタイマー処理のテストは難しいの?
まずは、なぜタイマー処理のテストがちょっと一筋縄ではいかないのか、その理由を身近な例えで考えてみましょう。
例え話:お買い物での「待ち時間」
あなたがお店で欲しいものを見つけたけど、レジがすごく混んでいて、「すみません、お会計まで5分ほどお待ちいただけますか?」と言われたと想像してみてください。
- 実際のタイマー処理: 実際の時間(5分)が経過するのを「本当に」待ちます。
- テストでのタイマー処理: テストを実行している間、本当に5分間、何もせずに待つのは、テストがすごく遅くなってしまいますよね。しかも、テストは秒単位で終わってほしいのが本音。
つまり、テストの世界では、実際の時間の流れに合わせるのではなく、「時間をスキップ」して、タイマーが「もう十分待ったよ!」と教えてくれるのを、すぐに確認したいんです。
`jest.useFakeTimers` って、どんな魔法?
ここで登場するのが、`jest.useFakeTimers` です!これは、JavaScriptのタイマー関数(`setTimeout`, `setInterval`, `requestAnimationFrame` など)の動きを、テスト環境だけで「ニセモノ」にしてくれる、とっても便利な機能なんです。
この「ニセモノ」のタイマーを使うと、
- 時間を一気に進める: 「はい、10秒進めて!」と指示するだけで、実際に10秒待つ必要がなくなります。
- タイマーを全部実行する: 「ここまででセットされたタイマー、全部動かして!」と指示できます。
まるで、おもちゃの時計の針をぐいっと進めるような感覚ですね。
まずは基本!`useEffect` と `setTimeout` のおさらい
テストの話に入る前に、`useEffect` と `setTimeout` の基本的な使い方をサクッと復習しておきましょう。
こんなコンポーネントを考えてみてください。
import React, { useState, useEffect } from ‘react’;
function GreetingMessage() {
const [message, setMessage] = useState(‘こんにちは!’);
useEffect(() => {
// 3秒後にメッセージを変更するタイマーを設定
const timerId = setTimeout(() => {
setMessage(‘こんにちは、Reactの世界へようこそ!’);
}, 3000); // 3000ミリ秒 = 3秒
// クリーンアップ関数:コンポーネントがアンマウントされるときにタイマーをクリアする
return () => {
clearTimeout(timerId);
};
}, []); // 空の依存配列なので、コンポーネントのマウント時に一度だけ実行される
return (
{message}
);
}
export default GreetingMessage;
このコンポーネントは、画面に表示されてから3秒後に「こんにちは!」というメッセージが「こんにちは、Reactの世界へようこそ!」に変わる、というシンプルなものです。
ここで重要なのが、`useEffect` の中で設定した `setTimeout` は、コンポーネントが画面から消えるとき(アンマウントされるとき)に、必ず `clearTimeout` で片付け(クリーンアップ)てあげる必要がある、ということです。これをしないと、メモリリークの原因になったり、予期せぬバグを引き起こしたりする可能性があります。
いよいよ本番!`jest.useFakeTimers` でタイマーをテストする
さて、いよいよこの `GreetingMessage` コンポーネントのタイマー処理をテストしてみましょう。
1. テストファイルの設定:魔法の準備!
まず、テストファイル(例えば `GreetingMessage.test.js`)の先頭で、`jest.useFakeTimers()` を呼び出します。これが「魔法の準備」です。
import React from ‘react’;
import { render, screen, act } from ‘@testing-library/react’;
import GreetingMessage from ‘./GreetingMessage’; // 上記のコンポーネントをインポート
// テストファイル全体でFake Timerを使用することを宣言
jest.useFakeTimers();
describe(‘GreetingMessage’, () => {
// ここにテストケースを書いていきます
});
2. 初期状態の確認:まずは「こんにちは!」であることを確かめる
テストの第一歩は、コンポーネントが最初に表示されたときの状態を確認することです。
import React from ‘react’;
import { render, screen, act } from ‘@testing-library/react’;
import GreetingMessage from ‘./GreetingMessage’;
jest.useFakeTimers();
describe(‘GreetingMessage’, () => {
test(‘初期状態では「こんにちは!」と表示されている’, () => {
render(
// screen.getByText() は、指定したテキストが表示されている要素を探します
expect(screen.getByText(‘こんにちは!’)).toBeInTheDocument();
});
});
このテストは、Fake Timerを使わなくても動きますね。なぜなら、まだタイマーが動く「時間」が経過していないからです。
3. タイマーが動くのを待って確認:ここが一番のポイント!
いよいよ、タイマーを動かして、メッセージが変わるのをテストします。
ここで使うのが、`jest.advanceTimersByTime(ms)` というメソッドです。これは、「指定したミリ秒数だけ時間を進める」という魔法です。
import React from ‘react’;
import { render, screen, act } from ‘@testing-library/react’;
import GreetingMessage from ‘./GreetingMessage’;
jest.useFakeTimers();
describe(‘GreetingMessage’, () => {
test(‘初期状態では「こんにちは!」と表示されている’, () => {
render(
expect(screen.getByText(‘こんにちは!’)).toBeInTheDocument();
});
// ここからが本番!
test(‘3秒後にメッセージが「こんにちは、Reactの世界へようこそ!」に変わる’, () => {
render(
// まずは初期状態を確認(念のため)
expect(screen.getByText(‘こんにちは!’)).toBeInTheDocument();
// ——————————————————–
// ★ここが魔法の瞬間!★
// 3000ミリ秒(3秒)だけ時間を進めます。
// 実際の3秒を待つ必要は全くありません!
// ——————————————————–
act(() => {
jest.advanceTimersByTime(3000);
});
// 時間を進めた後、メッセージが変わっているか確認します。
// screen.getByText は、要素が見つからない場合にエラーをthrowします。
// なので、もしメッセージが変わっていなければ、このテストは失敗します。
expect(screen.getByText(‘こんにちは、Reactの世界へようこそ!’)).toBeInTheDocument();
// さらに、元の「こんにちは!」が表示されていないことも確認すると、より確実です。
// queryByText は、要素が見つからない場合に null を返します。
expect(screen.queryByText(‘こんにちは!’)).not.toBeInTheDocument();
});
});
`act()` についての補足:
`act(() => { … })` で囲んでいるのは、Reactが状態の更新などを「アクト(実行)」する処理をまとめるためです。テストがより安定するために、Reactの公式ドキュメントでも推奨されている書き方なんです。タイマーの実行やUIの更新など、Reactの状態に影響を与える可能性のある処理は、なるべく `act` で囲むようにしましょう。
4. クリーンアップ処理もテストしよう!
`useEffect` の中で `clearTimeout` を使ったクリーンアップ処理も、きちんと動いているか確認しておきたいですよね。
`jest.useFakeTimers()` を使うと、タイマーのIDを取得したり、クリアされたかどうかをチェックしたりするのも簡単になります。
import React from ‘react’;
import { render, screen, act } from ‘@testing-library/react’;
import GreetingMessage from ‘./GreetingMessage’;
// spyOn を使って、特定の関数の呼び出しを監視します
const clearTimeoutSpy = jest.spyOn(window, ‘clearTimeout’);
jest.useFakeTimers();
describe(‘GreetingMessage’, () => {
// 各テストの前に、タイマーをクリアしておくと、テスト間の干渉を防げます
afterEach(() => {
jest.clearAllTimers(); // 設定したタイマーをすべてクリア
clearTimeoutSpy.mockClear(); // spyOnの呼び出し履歴をクリア
});
test(‘初期状態では「こんにちは!」と表示されている’, () => {
render(
expect(screen.getByText(‘こんにちは!’)).toBeInTheDocument();
});
test(‘3秒後にメッセージが「こんにちは、Reactの世界へようこそ!」に変わる’, () => {
render(
act(() => {
jest.advanceTimersByTime(3000);
});
expect(screen.getByText(‘こんにちは、Reactの世界へようこそ!’)).toBeInTheDocument();
expect(screen.queryByText(‘こんにちは!’)).not.toBeInTheDocument();
});
// ★クリーンアップ処理のテスト★
test(‘コンポーネントがアンマウントされるときにタイマーがクリアされる’, () => {
const { unmount } = render(
// コンポーネントをアンマウントする
unmount();
// clearTimeout が呼び出されたかどうかを確認します。
// 1回以上呼び出されていれば true になります。
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
});
});
`jest.spyOn` と `afterEach` について:
- `jest.spyOn(window, ‘clearTimeout’)`: これは、ブラウザの `window` オブジェクトにある `clearTimeout` 関数が、テスト中に「いつ、何回、どんな引数で」呼ばれたかを監視するためのものです。
- `afterEach(() => { … })`: これは、各テストケース(`test(…)` のブロック)が実行された後に、必ず実行される処理を記述する場所です。
- `jest.clearAllTimers()`: これで、そのテストケースで設定されたタイマーをすべてリセットします。次のテストケースに影響を与えないようにするためです。
- `clearTimeoutSpy.mockClear()`: `spyOn` で監視している関数が、前回のテストで呼ばれた履歴をクリアします。
このように `afterEach` で後始末をしておくと、テストが独立して実行され、より信頼性の高いテスト結果を得ることができます。
`setInterval` の場合も基本は同じ!
`setInterval` の場合も、基本的な考え方は `setTimeout` と同じです。
例えば、1秒ごとにカウンターが増えるコンポーネントがあったとしましょう。
import React, { useState, useEffect } from ‘react’;
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000); // 1秒ごとに実行
return () => {
clearInterval(intervalId); // clearIntervalでクリア
};
}, []);
return (
Count: {count}
);
}
export default Counter;
このテストは、`jest.advanceTimersByTime(ms)` で時間を進め、その都度 `count` の値が期待通りに更新されているかを確認します。
import React from ‘react’;
import { render, screen, act } from ‘@testing-library/react’;
import Counter from ‘./Counter’;
jest.useFakeTimers();
describe(‘Counter’, () => {
test(‘初期状態ではCount: 0と表示されている’, () => {
render(
expect(screen.getByText(‘Count: 0’)).toBeInTheDocument();
});
test(‘1秒ごとにカウントが増える’, () => {
render(
// 1秒進める
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText(‘Count: 1’)).toBeInTheDocument();
// さらに1秒進める(合計2秒経過)
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText(‘Count: 2’)).toBeInTheDocument();
// もし元のCount: 1が表示されていないことも確認したい場合
expect(screen.queryByText(‘Count: 1’)).not.toBeInTheDocument();
});
test(‘コンポーネントがアンマウントされるときにタイマーがクリアされる’, () => {
const clearIntervalSpy = jest.spyOn(window, ‘clearInterval’);
const { unmount } = render(
unmount();
expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
// spyOnをクリアしておく
clearIntervalSpy.mockRestore(); // mockRestore()で元の関数に戻す
});
});
`jest.advanceTimersByTime()` は、指定した時間だけ「まとめて」進めるので、`setInterval` のように繰り返し実行される処理も、時間を複数回進めることで、その都度の状態を確認できます。
まとめ:タイマーテストは怖くない!
どうでしたか? `jest.useFakeTimers` を使うと、まるで時間を操る魔法使いになったみたいで、タイマー処理のテストがずっと楽になるのが分かっていただけたのではないでしょうか。
- `jest.useFakeTimers()`: テスト環境でタイマーを「ニセモノ」にするおまじない。
- `jest.advanceTimersByTime(ms)`: 時間をスキップして、タイマーを先に進める魔法。
- `act(() => { … })`: Reactの状態更新を安全に行うための「実行ブロック」。
- `jest.spyOn()` と `afterEach()`: クリーンアップ処理がちゃんと動いているか確認し、テストをきれいに保つためのテクニック。
これらのテクニックを使いこなせば、`useEffect` 内のタイマー処理も、自信を持ってテストできるようになります。最初は少し戸惑うかもしれませんが、実際にコードを書いて動かしてみると、その便利さがきっと実感できるはずです。
React開発は、時に「え、なんでこうなるの?」と立ち止まることもありますが、こういった便利なツールや、ちょっとした「なるほど!」を知ることで、開発がぐっと楽しく、そしてスムーズになります。
今日の解説が、あなたのReact開発の一助となれば幸いです! もし「ここがもっと知りたい!」という点があれば、ぜひコメントなどで教えてくださいね。

コメント