はじめに
この記事では、Next.js プロジェクトでの Jest を使ったテスト方法について解説します。モック関数の基本から、サーバーコンポーネントのテストまで、実践的なテクニックをまとめています。
他にもこういう方法あるよ!などアドバイスがあればコメントいただきたいです🙏
目次
- モック関数の基本
- モック関数の戻り値設定
- モック関数のリセット
- .resolves / .rejects
- タイマー機能のモック
- コンポーネントのモック
- ライブラリの戻り値をモック
- モジュールのモック化手法
- 特定要素内の要素検索 - within
- サーバーコンポーネントのテスト
- 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: "データの取得に失敗しました"
})
mockImplementation
モック関数の処理内容をカスタマイズしたい場合はmockImplementation
を使用します。
const loginMock = jest.fn().mockImplementation(() => {
// ログイン処理のカスタム実装
return Promise.resolve({ success: true, userId: 123 })
})
// Tips: モック関数を意図的に永続的な待機状態にして、処理中を維持する
const loginMock = jest.requireMock('@/actions/login').login;
loginMock.mockImplementation(() => new Promise(() => {
// 意図的に解決しないPromiseを返すことでログイン処理を永続的な待機状態にする
}));
モック関数のリセット
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
の第二引数に戻り値を指定します。
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();
});
モジュールのモック化手法
jest.mock
jest.mock
はモジュール全体をモック化するために使用します。第2引数に戻り値を示す関数を記載することで、モジュール全体の振る舞いをカスタマイズできます。
// ユーザーAPI全体をモック化
jest.mock('@/api/user', () => ({
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test User' }),
updateUser: jest.fn().mockResolvedValue({ success: true }),
deleteUser: jest.fn().mockResolvedValue({ success: true })
}));
jest.fn
jest.fn
は特定の関数をピンポイントでモック化したい時に利用します。テスト対象のコードに関数を渡して、その呼び出しを追跡したい場合に便利です。
// 単一の関数をモック化
const mockCallback = jest.fn(x => x + 42);
[0, 1].forEach(mockCallback);
expect(mockCallback.mock.calls.length).toBe(2);
expect(mockCallback.mock.calls[0][0]).toBe(0);
expect(mockCallback.mock.results[0].value).toBe(42);
jest.requireMock
jest.requireMock
はモック化されたモジュールを取得するための関数です。モック化したモジュールを取得して操作したい場合に使用します。
// モジュールをモック化
jest.mock('@/utils/math');
// モック化されたモジュールを取得
const mathMock = jest.requireMock('@/utils/math');
mathMock.add.mockImplementation((a, b) => a * b); // addの実装を乗算に置き換え
// テスト実行
expect(mathMock.add(2, 3)).toBe(6); // 2 * 3 = 6
jest.requireActual
jest.requireActual
は実際の(モック化されていない)本物のモジュールを取得する関数です。モック化されていないオリジナルの実装が必要な場合に使用します。
// モジュールの一部だけをモック化したい場合
jest.mock('@/utils/formatting', () => {
// 実際のモジュールを取得
const actual = jest.requireActual('@/utils/formatting');
return {
// 実際のメソッドをそのまま使用
...actual,
// formatDateだけをモック化
formatDate: jest.fn().mockReturnValue('2023-01-01')
};
});
特定要素内の要素検索 - within
within
を使うと、特定の要素内に絞って要素検索を行うことができます。getBy、findByなどで「要素A内にある要素Bを取得する」場合に便利です。
// フォーム要素を取得
const formElement = screen.getByRole('form');
// フォーム内のボタンを取得して検証
expect(
within(formElement).getByRole('button', { name: 'ログイン' }),
).toBeInTheDocument();
// ダイアログ内の特定のテキストを検索
const dialog = screen.getByRole('dialog');
expect(
within(dialog).getByText('設定を保存しますか?')
).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", () => ({
const actualNavigation = jest.requireActual('next/navigation');
return {
...actualNavigation,
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(() => {
// モック化されたモジュールにアクセスする
const { getMyPosts } = jest.requireMock("@/app/(private)/account/fetcher");
// 記事一覧が返されるようにモック設定
(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を使ったテストを効率的に行うための主要なテクニックを紹介しました。モック関数の基本から応用、コンポーネントテストの手法まで幅広く解説しています。テストの実装により品質の高いNext.jsアプリケーションを開発しましょう。
(Vibe Coding で書くことが多かったので、理解が不足している点が多かった...)