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

Jest + React Testing Library でカスタムフックをテストする

renderHook を使ってカスタムフックの動作を単体テストする例。act でステート更新をラップし、非同期フックの検証パターンも示す。

nextjstestingjest

対応バージョン

nextjs 15react 19jest 29

前提環境

Jest の基本的な使い方と React のカスタムフックの仕組みを理解していること

概要

@testing-library/reactrenderHookact を使い、カスタムフックの動作を単体テストする。同期フックのステート更新テストと、非同期フック(waitFor を使った API フェッチ)のテストパターンを示す。

インストール

npm install jest @types/jest ts-jest
npm install --save-dev @testing-library/react @testing-library/jest-dom jest-environment-jsdom

実装

jest.config.ts

// jest.config.ts
import type { Config } from "jest";

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterFramework: ["<rootDir>/jest.setup.ts"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
};

export default config;

jest.setup.ts

// jest.setup.ts
import "@testing-library/jest-dom";

テスト対象: useCounter フック

// hooks/useCounter.ts
import { useState } from "react";

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

同期フックのテスト

// hooks/useCounter.test.ts
import { act, renderHook } from "@testing-library/react";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  it("初期値を返す", () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it("increment で count が増える", () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it("decrement で count が減る", () => {
    const { result } = renderHook(() => useCounter(3));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(2);
  });

  it("reset で初期値に戻る", () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});

非同期フックのテスト

// hooks/useFetch.ts
import { useEffect, useState } from "react";

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error("fetch failed");
        return res.json() as Promise<T>;
      })
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}
// hooks/useFetch.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { useFetch } from "./useFetch";

global.fetch = jest.fn();

describe("useFetch", () => {
  it("データを取得して返す", async () => {
    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => ({ id: 1, title: "テスト" }),
    });

    const { result } = renderHook(() => useFetch("/api/item"));

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toEqual({ id: 1, title: "テスト" });
    expect(result.current.error).toBeNull();
  });

  it("エラー時に error をセットする", async () => {
    (fetch as jest.Mock).mockResolvedValueOnce({ ok: false });

    const { result } = renderHook(() => useFetch("/api/item"));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBeInstanceOf(Error);
    expect(result.current.data).toBeNull();
  });
});

ポイント

  • renderHook でカスタムフックをコンポーネントなしにテストできる。result.current から戻り値を参照する
  • ステートを更新する操作は act でラップする。これにより React の更新処理が同期的に完了してから assertion できる
  • 非同期フックは waitFor を使い、条件が満たされるまで再チェックする
  • fetch をモックする場合は global.fetch = jest.fn() で差し替え、mockResolvedValueOnce でレスポンスを定義する
  • jest.config.tstestEnvironment: 'jest-environment-jsdom' を指定することでブラウザ API(DOM)が使える

注意点

Next.js 15 では jest.config を ts で書ける。@testing-library/react の renderHook は React 18 以降で import 先が変わっている点に注意。

関連サンプル