はじめに
この記事では、Next.js プロジェクトでの Jest を使ったテスト方法について解説します。モック関数の基本から、サーバーコンポーネントのテストまで、実践的なテクニックをまとめています。
他にもこういう方法あるよ!などアドバイスがあればコメントいただきたいです🙏
目次
- モック関数の基本
- モック関数の戻り値設定
- モック関数のリセット
- .resolves / .rejects
- タイマー機能のモック
- コンポーネントのモック
- ライブラリの戻り値をモック
- サーバーコンポーネントのテスト
- useRouter, useSearchParams をモック化
- 特定のテストケースで条件を切り替える
モック関数の基本
モック化
const handleClick = jest.fn()
関数が呼ばれたことの検証
// 関数が呼び出されたかどうかを確認
expect(handleClick).toHaveBeenCalled()
// 関数が指定回呼び出されたかを確認
expect(handleClick).toHaveBeenCalledTimes(2)
// 関数が特定の引数で呼び出されたかを確認
expect(handleClick).toHaveBeenCalledWith('引数')
モック関数の戻り値設定
mockReturnValue
mock関数を呼び出した際の返り値を指定する際はmockReturnValue
を使います。
describe("jest.fn()",()=>{
it("mockReturnValue",()=>{
const mockFunction = jest.fn().mockReturnValue("Hello mock")
console.log(mockFunction()) // "Hello mock"が出力される
})
})
mockResolveValue
mock関数の返り値として、Promise(resolve)を設定したい場合はmockResolvedValue
を使います。
主に非同期APIのモック化に利用されます。
const sampleFn = jest.fn().mockResolvedValue({
a: 1
})
expect(sampleFn()).resolves.toEqual({
a: 1
})
mockRejectValue
mock関数の返り値として、Promise(reject)を返したい場合はmockRejectedValue
を使います。
APIエラーのテストなどで用いられます。
const sampleFn = jest.fn().mockRejectedValue({
error: "データの取得に失敗しました"
})
expect(sampleFn()).rejects.toEqual({
error: "データの取得に失敗しました"
})
モック関数のリセット
mockClear
mockClear
はモック関数の呼び出し履歴(.mock.calls
や .mock.results
など)をリセットします。
ただし、モック関数の動作自体(返り値など)はリセットされません。テストケース間でモック関数の呼び出し履歴をクリアしたい場合に使用します。
const mockFn = jest.fn().mockReturnValue("テスト");
mockFn();
expect(mockFn.mock.calls.length).toBe(1); // 呼び出し回数は1
mockFn.mockClear();
expect(mockFn.mock.calls.length).toBe(0); // 呼び出し回数は0にリセット
expect(mockFn()).toBe("テスト"); // 返り値は変わらない
mockReset
mockReset
は mockClear
の機能に加えて、モック関数の動作(返り値など)もリセットします。
const mockFn = jest.fn().mockReturnValue("テスト");
console.log(mockFn()); // "テスト"
mockFn.mockReset();
console.log(mockFn()); // undefined
.resolves / .rejects
expect
宣言で .resolves
マッチャを使うこともでき、Jestはそのpromiseが解決されるまで待機します。
promise
が reject
された場合は、テストは自動的に失敗します。
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
タイマー機能のモック
setTimeout
や setInterval
などの時間に関わる関数をテストする場合、Jestはタイマーをモック化する機能を提供しています。
// jest.useFakeTimers 偽のタイマーを有効化
// jest.advanceTimersByTime 指定した時間分タイマーを進める
// jest.useRealTimers タイマーを通常の動作に戻す(useFakeTimersはすべてのタイマーの動作に影響するためテスト完了後に必ず実行する)
describe('タイマーテスト', () => {
beforeEach(() => {
jest.useFakeTimers(); // 偽のタイマーを有効化
});
afterEach(() => {
jest.useRealTimers(); // テスト後に通常の動作に戻す
});
test('500ms後にコールバックが実行される', () => {
const callback = jest.fn();
setTimeout(callback, 500);
expect(callback).not.toHaveBeenCalled();
// 時間を500ms進める
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
});
コンポーネントのモック
コンポーネントをモック化したい場合、関数としてモック化します。
// Searchコンポーネントをモック化
jest.mock('@/components/Search', () => {
return function MockSearchBox() {
return <div data-testid="search-box">SearchBox</div>
}
});
it('検索ボックスが表示される', () => {
render(<PageWithSearchBox />);
expect(screen.getByTestId('search-box')).toBeInTheDocument();
});
コンポーネントの export
によってmock方法が変わります。
export default
でエクスポートされている場合は上記のように関数を直接返せますが、export const
でエクスポートされている場合は、以下のようにオブジェクトとして返す必要があります。
jest.mock('@/components/Search', () => ({
SearchBox: function MockSearchBox() {
return <div data-testid="search-box">SearchBox</div>;
}
}));
ライブラリの戻り値をモック
APIやサービスの呼び出し結果をモック化したい場合に使用します。
jest.mock('@/lib/api', () => ({
// getArticlesは常に特定の記事一覧を返すようにする
getArticles: () => Promise.resolve([
{ id: 1, title: 'テスト記事1' },
{ id: 2, title: 'テスト記事2' }
]),
// createArticleは成功レスポンスを返すようにする
createArticle: () => Promise.resolve({ success: true, id: 999 })
}));
it('記事一覧が表示される', async () => {
render(<ArticleList />);
expect(await screen.findByText('テスト記事1')).toBeInTheDocument();
expect(screen.getByText('テスト記事2')).toBeInTheDocument();
});
サーバーコンポーネントのテスト
Next.js 13以降のサーバーコンポーネントは非同期関数として実装されるため、テストでは await
を使ってレンダリングする必要があります。
it('Server Components', async () => {
render(await ServerComponent());
expect(screen.getByText('サーバーコンポーネント')).toBeInTheDocument();
});
useRouter, useSearchParams をモック化
Next.js
の useRouter
や useSearchParams
などのフックをモック化する方法です。
import { useRouter, useSearchParams } from "next/navigation";
// next/navigationモジュールをモック化
jest.mock("next/navigation", () => ({
useSearchParams: jest.fn(),
useRouter: jest.fn(),
}));
// テスト内でモックの戻り値を設定
beforeEach(() => {
// useSearchParamsの戻り値を設定
(useSearchParams as jest.Mock).mockReturnValue({
get: jest.fn().mockReturnValue(''),
});
// useRouterの戻り値を設定
(useRouter as jest.Mock).mockReturnValue({
push: jest.fn(),
});
});
特定のテストケースで条件を切り替える
APIレスポンスに応じたUIの表示をテストする例です。
beforeEach
を使って各テストケースで異なるモックを設定しています。
import { render, screen } from "@testing-library/react";
import MyPostsList from "./MyPostsList";
// API関数をモック化
jest.mock('@/app/(private)/account/fetcher', () => ({
getMyPosts: jest.fn()
}));
describe('初期表示', () => {
beforeEach(() => {
// 記事一覧が返されるようにモック設定
(getMyPosts as jest.Mock).mockResolvedValue({
json: jest.fn().mockResolvedValue({
posts: [
{
id: 1,
title: 'テストタイトル1',
published: true,
},
],
}),
});
});
it('記事の内容が表示される', async () => {
render(await MyPostsList({ currentPage: 1, perPage: 10 }));
expect(screen.getByText('テストタイトル1')).toBeInTheDocument();
});
});
describe('記事が存在しない場合', () => {
beforeEach(() => {
// モック化されたモジュールにアクセスする
const { getMyPosts } = jest.requireMock("@/app/(private)/account/fetcher");
// 空の記事一覧が返されるようにモック設定
(getMyPosts as jest.Mock).mockResolvedValue({
json: jest.fn().mockResolvedValue({
posts: [],
}),
});
});
it('記事がありませんと表示される', async () => {
render(await MyPostsList({ currentPage: 1, perPage: 10 }));
expect(screen.getByText('記事がありません。')).toBeInTheDocument();
});
});
まとめ
Next.jsアプリケーションでJestを使ったテストを効率的に行うための主要なテクニックを紹介しました。
(Vibe Coding で書くことが多かったので、理解が不足している点が多かった...)