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

Jest でモジュールをモックして単体テストを書く

jest.mock でモジュール・外部依存を差し替え、spy 関数で呼び出しを検証する例。API クライアントや Next.js の navigation モジュールをモックするパターンを示す。

nextjstestingjest

対応バージョン

nextjs 15react 19jest 29

前提環境

Jest の基本的なテストの書き方を理解していること

概要

jest.mock() でモジュール全体を差し替えることで、外部依存(API クライアント・Next.js の next/navigation 等)を単体テストから切り離す。jest-custom-hook-testglobal.fetch モックとは異なり、任意のモジュールの関数・クラスをすべて制御できる。

インストール

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

実装

パターン 1: API クライアントモジュールの差し替え

// lib/api.ts
export async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("User not found");
  return res.json() as Promise<{ id: string; name: string }>;
}
// components/UserCard.tsx
"use client";

import { useEffect, useState } from "react";
import { fetchUser } from "@/lib/api";

export function UserCard({ userId }: { userId: string }) {
  const [name, setName] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUser(userId)
      .then((u) => setName(u.name))
      .catch((e) => setError(e.message));
  }, [userId]);

  if (error) return <p role="alert">{error}</p>;
  if (!name) return <p>読み込み中...</p>;
  return <p data-testid="user-name">{name}</p>;
}
// components/UserCard.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { UserCard } from "./UserCard";
import * as api from "@/lib/api";

// モジュール全体をモック
jest.mock("@/lib/api");

const mockedFetchUser = jest.mocked(api.fetchUser);

describe("UserCard", () => {
  it("ユーザー名を表示する", async () => {
    mockedFetchUser.mockResolvedValueOnce({ id: "1", name: "山田太郎" });

    render(<UserCard userId="1" />);

    await waitFor(() => {
      expect(screen.getByTestId("user-name")).toHaveTextContent("山田太郎");
    });
    expect(mockedFetchUser).toHaveBeenCalledWith("1");
  });

  it("エラー時にアラートを表示する", async () => {
    mockedFetchUser.mockRejectedValueOnce(new Error("User not found"));

    render(<UserCard userId="999" />);

    await waitFor(() => {
      expect(screen.getByRole("alert")).toHaveTextContent("User not found");
    });
  });
});

パターン 2: next/navigation のモック

// components/BackButton.tsx
"use client";

import { useRouter } from "next/navigation";

export function BackButton() {
  const router = useRouter();
  return (
    <button onClick={() => router.back()}>戻る</button>
  );
}
// components/BackButton.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BackButton } from "./BackButton";

// next/navigation をモジュールごと差し替え
jest.mock("next/navigation", () => ({
  useRouter: jest.fn(),
}));

import { useRouter } from "next/navigation";
const mockedUseRouter = jest.mocked(useRouter);

describe("BackButton", () => {
  it("クリックで router.back() が呼ばれる", async () => {
    const back = jest.fn();
    mockedUseRouter.mockReturnValue({ back } as ReturnType<typeof useRouter>);

    const user = userEvent.setup();
    render(<BackButton />);

    await user.click(screen.getByRole("button", { name: "戻る" }));

    expect(back).toHaveBeenCalledTimes(1);
  });
});

パターン 3: 一部の関数だけをスパイ(spyOn)

// lib/logger.ts
export function log(message: string) {
  console.log(`[LOG] ${message}`);
}

export function warn(message: string) {
  console.warn(`[WARN] ${message}`);
}
// lib/logger.test.ts
import * as logger from "./logger";

describe("logger", () => {
  it("log が console.log を呼ぶ", () => {
    const spy = jest.spyOn(console, "log").mockImplementation(() => {});

    logger.log("テストメッセージ");

    expect(spy).toHaveBeenCalledWith("[LOG] テストメッセージ");
    spy.mockRestore();
  });
});

ポイント

  • jest.mock("@/lib/api") はファイルの先頭(import より前)に巻き上げられる。変数への参照は jest.mocked() で型付きにする
  • jest.mocked(fn) は TypeScript で jest.Mock 型に安全にキャストする。fn as jest.Mock より推奨
  • next/navigationnext/router はファクトリ関数 () => ({ useRouter: jest.fn() }) 形式でモックすると ESM 互換になる
  • jest.spyOn は元の実装を残しつつ呼び出しを検証したい場合に使う。テスト後は mockRestore() で元に戻すことを忘れない
  • jest-component-test との使い分け: UI の表示・操作を検証するなら render + userEvent、外部依存の差し替えと呼び出し検証が主目的なら jest.mock が中心になる

注意点

jest-custom-hook-test は global.fetch のみモック。これは jest.mock('module') でモジュール全体を差し替えるパターン(Next.js の useRouter / next/navigation のモックを含む)。

関連サンプル