レビュー待ち·難易度: 中級·更新: 2026-04-18

Jest の useFakeTimers でデバウンス・タイマー処理をテストする

jest.useFakeTimers() と jest.advanceTimersByTime() を使って、setTimeout / setInterval / デバウンス関数の時間依存処理を同期的にテストする例。

nextjstestingjest

対応バージョン

nextjs 15react 19jest 29

前提環境

Jest の基本テスト記法と jest.fn() モックの使い方を理解していること

概要

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/reactact()jest.advanceTimersByTime を組み合わせる。このサンプルは純粋なユーティリティ関数の時間制御テストに特化

注意点

jest-component-test は @testing-library/react を使ったコンポーネント操作テスト。jest-mock-module はモジュール差し替え。jest-route-handler-test は Route Handler 単体テスト。jest-snapshot-test はスナップショット比較。これは useFakeTimers による時間制御に特化し、debounce / interval など非同期タイマー処理の同期的テストパターンを示す。

関連サンプル