概要
jest.mock() でモジュール全体を差し替えることで、外部依存(API クライアント・Next.js の next/navigation 等)を単体テストから切り離す。jest-custom-hook-test の global.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/navigationやnext/routerはファクトリ関数() => ({ useRouter: jest.fn() })形式でモックすると ESM 互換になるjest.spyOnは元の実装を残しつつ呼び出しを検証したい場合に使う。テスト後はmockRestore()で元に戻すことを忘れないjest-component-testとの使い分け: UI の表示・操作を検証するならrender + userEvent、外部依存の差し替えと呼び出し検証が主目的ならjest.mockが中心になる