概要
jest.useFakeTimers() でシステムタイマーを乗っ取り、jest.advanceTimersByTime() で時間を擬似的に進めることで、setTimeout / setInterval / デバウンス関数の時間依存処理を実際に待機せず同期的にテストする。
インストール
npm install --save-dev jest @types/jest ts-jest
実装
テスト対象のユーティリティ関数
// lib/timer-utils.ts
// デバウンス関数
export function debounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 一定間隔で callback を呼び続ける polling
export function startPolling(
callback: () => void,
interval: number
): () => void {
const id = setInterval(callback, interval);
return () => clearInterval(id); // stop 関数を返す
}
// delay 後に一度だけ実行
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
setTimeout のテスト
// lib/__tests__/timer-utils.test.ts
import { debounce, startPolling } from "@/lib/timer-utils";
describe("debounce", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers(); // テスト後は必ず元に戻す
});
it("delay 前は呼ばれない", () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 300);
debouncedFn();
expect(fn).not.toHaveBeenCalled();
// 200ms 進める(300ms に満たない)
jest.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
});
it("delay 後に 1 回だけ呼ばれる", () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 300);
debouncedFn();
jest.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledTimes(1);
});
it("連続呼び出しはタイマーがリセットされる", () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 300);
debouncedFn(); // t=0
jest.advanceTimersByTime(200);
debouncedFn(); // t=200: タイマーリセット
jest.advanceTimersByTime(200); // t=400 (リセット後 200ms)
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(100); // t=500 (リセット後 300ms)
expect(fn).toHaveBeenCalledTimes(1);
});
});
setInterval のテスト
// lib/__tests__/timer-utils.test.ts(続き)
describe("startPolling", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("interval ごとに callback が呼ばれる", () => {
const callback = jest.fn();
startPolling(callback, 1000);
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
});
it("stop 関数を呼ぶと polling が止まる", () => {
const callback = jest.fn();
const stop = startPolling(callback, 1000);
jest.advanceTimersByTime(2000);
stop(); // ここで停止
jest.advanceTimersByTime(2000); // 停止後は呼ばれない
expect(callback).toHaveBeenCalledTimes(2);
});
});
Promise ベースのタイマーテスト
// lib/__tests__/timer-utils.test.ts(続き)
import { delay } from "@/lib/timer-utils";
describe("delay", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("指定時間後に resolve する", async () => {
let resolved = false;
delay(500).then(() => {
resolved = true;
});
jest.advanceTimersByTime(499);
// Promise のマイクロタスクを処理
await Promise.resolve();
expect(resolved).toBe(false);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(resolved).toBe(true);
});
});
jest.runAllTimers / jest.runOnlyPendingTimers の使い分け
describe("タイマー一括実行", () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it("runAllTimers: キューの全タイマーを即時実行", () => {
const fn = jest.fn();
setTimeout(fn, 10000); // 10秒後のタイマー
jest.runAllTimers(); // 時間を進めずに全タイマー実行
expect(fn).toHaveBeenCalledTimes(1);
});
it("runOnlyPendingTimers: 現在待機中のタイマーのみ実行(再帰タイマーは1周のみ)", () => {
const fn = jest.fn();
function recursiveTimer() {
fn();
setTimeout(recursiveTimer, 100); // 再帰
}
setTimeout(recursiveTimer, 100);
jest.runOnlyPendingTimers(); // 最初の1回だけ実行(無限ループしない)
expect(fn).toHaveBeenCalledTimes(1);
});
});
ポイント
jest.useFakeTimers()をbeforeEachで呼び、jest.useRealTimers()をafterEachで呼ぶ。テスト間の副作用を防ぐために必ずペアで使うjest.advanceTimersByTime(ms)で指定ミリ秒だけ時間を進める。setTimeout/setIntervalの時間を実際に待たず同期的に制御できるjest.runAllTimers()は全タイマーを即時実行するが、再帰的なsetTimeoutがあると無限ループする。再帰タイマーにはjest.runOnlyPendingTimers()を使う- Promise ベースのタイマー(async/await 内の
setTimeout)はjest.advanceTimersByTime後にawait Promise.resolve()でマイクロタスクキューを処理してから assertion する jest-component-testとの使い分け: コンポーネントの debounce 入力テストは@testing-library/reactのact()とjest.advanceTimersByTimeを組み合わせる。このサンプルは純粋なユーティリティ関数の時間制御テストに特化