【Next.js】Jest でのテスト方法完全ガイド - モック関数からサーバーコンポーネントまで

## はじめに

この記事では、Next.js プロジェクトでの Jest を使ったテスト方法について解説します。モック関数の基本から、サーバーコンポーネントのテストまで、実践的なテクニックをまとめています。

他にもこういう方法あるよ！などアドバイスがあればコメントいただきたいです🙏

## 目次

- [モック関数の基本](#モック関数の基本)
- [モック関数の戻り値設定](#モック関数の戻り値設定)
- [モック関数のリセット](#モック関数のリセット)
- [.resolves / .rejects](#.resolves---.rejects)
- [タイマー機能のモック](#タイマー機能のモック)
- [コンポーネントのモック](#コンポーネントのモック)
- [ライブラリの戻り値をモック](#ライブラリの戻り値をモック)
- [モジュールのモック化手法](#モジュールのモック化手法)
- [特定要素内の要素検索 - within](#特定要素内の要素検索---within)
- [サーバーコンポーネントのテスト](#サーバーコンポーネントのテスト)
- [useRouter, useSearchParams をモック化](#userouter-usesearchparams-をモック化)
- [特定のテストケースで条件を切り替える](#特定のテストケースで条件を切り替える)

## モック関数の基本

### モック化

```javascript
const handleClick = jest.fn()
```

### 関数が呼ばれたことの検証

```typescript
// 関数が呼び出されたかどうかを確認
expect(handleClick).toHaveBeenCalled()

// 関数が指定回呼び出されたかを確認
expect(handleClick).toHaveBeenCalledTimes(2)

// 関数が特定の引数で呼び出されたかを確認
expect(handleClick).toHaveBeenCalledWith('引数')
```

## モック関数の戻り値設定

### mockReturnValue

mock関数を呼び出した際の返り値を指定する際は`mockReturnValue`を使います。

```typescript
describe("jest.fn()",()=>{
  it("mockReturnValue",()=>{
      const mockFunction = jest.fn().mockReturnValue("Hello mock")
      console.log(mockFunction()) // "Hello mock"が出力される
   })
})
```

### mockResolveValue

mock関数の返り値として、Promise（resolve）を設定したい場合は`mockResolvedValue`を使います。

主に非同期APIのモック化に利用されます。

```javascript
const sampleFn = jest.fn().mockResolvedValue({
  a: 1
})

expect(sampleFn()).resolves.toEqual({
  a: 1
})
```

### mockRejectValue

mock関数の返り値として、Promise（reject）を返したい場合は`mockRejectedValue`を使います。

APIエラーのテストなどで用いられます。

```typescript
const sampleFn = jest.fn().mockRejectedValue({
  error: "データの取得に失敗しました"
})

expect(sampleFn()).rejects.toEqual({
  error: "データの取得に失敗しました"
})
```

### mockImplementation

モック関数の処理内容をカスタマイズしたい場合は`mockImplementation`を使用します。

```typescript
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` など）をリセットします。

ただし、モック関数の動作自体（返り値など）はリセットされません。テストケース間でモック関数の呼び出し履歴をクリアしたい場合に使用します。

```typescript
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` の機能に加えて、モック関数の動作（返り値など）もリセットします。

```typescript
const mockFn = jest.fn().mockReturnValue("テスト");
console.log(mockFn()); // "テスト"

mockFn.mockReset();
console.log(mockFn()); // undefined
```

## .resolves / .rejects

`expect` 宣言で `.resolves` マッチャを使うこともでき、Jestはそのpromiseが解決されるまで待機します。
`promise`が `reject` された場合は、テストは自動的に失敗します。

```javascript
test('the data is peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});
```

## タイマー機能のモック

`setTimeout` や `setInterval` などの時間に関わる関数をテストする場合、Jestはタイマーをモック化する機能を提供しています。

```javascript
// 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);
  });
});
```

## コンポーネントのモック

コンポーネントをモック化したい場合、関数としてモック化します。

```typescript
// 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();
});
```

:::note warn
コンポーネントの `export` によってmock方法が変わります。

`export default` でエクスポートされている場合は上記のように関数を直接返せますが、`export const` でエクスポートされている場合は、以下のようにオブジェクトとして返す必要があります。
```tsx
jest.mock('@/components/Search', () => ({
  SearchBox: function MockSearchBox() {
    return <div data-testid="search-box">SearchBox</div>;
  }
}));
```
:::

## ライブラリの戻り値をモック

APIやサービスの呼び出し結果をモック化したい場合に使用します。
基本的にコンポーネントのモック化と同様、 `jest.mock` の第二引数に戻り値を指定します。

```typescript
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引数に戻り値を示す関数を記載することで、モジュール全体の振る舞いをカスタマイズできます。

```typescript
// ユーザー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`は特定の関数をピンポイントでモック化したい時に利用します。テスト対象のコードに関数を渡して、その呼び出しを追跡したい場合に便利です。

```typescript
// 単一の関数をモック化
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`はモック化されたモジュールを取得するための関数です。モック化したモジュールを取得して操作したい場合に使用します。

```typescript
// モジュールをモック化
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`は実際の（モック化されていない）本物のモジュールを取得する関数です。モック化されていないオリジナルの実装が必要な場合に使用します。

```typescript
// モジュールの一部だけをモック化したい場合
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を取得する」場合に便利です。

```typescript
// フォーム要素を取得
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` を使ってレンダリングする必要があります。

```typescript
it('Server Components', async () => {
  render(await ServerComponent());
  
  expect(screen.getByText('サーバーコンポーネント')).toBeInTheDocument();
});
```

## useRouter, useSearchParams をモック化

`Next.js` の `useRouter` や `useSearchParams` などのフックをモック化する方法です。

```typescript
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` を使って各テストケースで異なるモックを設定しています。

```typescript
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 で書くことが多かったので、理解が不足している点が多かった...）