Next.js 15 + Jest + MSW でネットワークレベルAPIモックテストを実現する

に公開

Next.js 15 + Jest + MSW でネットワークレベルAPIモックテストを実現する

📖 概要

このガイドでは、Next.js 15プロジェクトにMSW(Mock Service Worker)を導入し、JestでネットワークレベルのAPIモックテストを実現する方法を詳しく解説します。

🎯 目標

  • ネットワークレベルモック: fetch APIが実際に呼ばれ、MSWがレスポンスを返す
  • 型安全なテスト環境: TypeScriptによる型保証
  • 保守性の高いテスト: テストケースの追加・変更が容易
  • Next.js 15対応: App Routerやその他機能との互換

⚠️ 解決した課題

  1. Jest環境でのMSW実行エラー

    ReferenceError: Response is not defined
    ReferenceError: TextEncoder is not defined
    ReferenceError: ReadableStream is not defined
    ...etc
    

🔧 実装手順

1. 依存関係のインストール

npm install --save-dev msw undici @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom

なぜこれらが必要か?

  • msw: ネットワークレベルAPIモック
  • undici: Node.js環境でのfetch API提供
  • @testing-library/*: React Component Testing
  • jest: テストフレームワーク
  • jest-environment-jsdom: ブラウザ環境のシミュレーション

2. ポリフィルファイルの作成

ファイル: jest.polyfills.js

// jest.polyfills.js
// MSW用のポリフィル設定

/* eslint-disable @typescript-eslint/no-require-imports */
const { TextDecoder, TextEncoder } = require('util');
const { ReadableStream, TransformStream } = require('stream/web');

// Web APIs polyfills
if (!globalThis.TextDecoder) {
  globalThis.TextDecoder = TextDecoder;
}
if (!globalThis.TextEncoder) {
  globalThis.TextEncoder = TextEncoder;
}
if (!globalThis.ReadableStream) {
  globalThis.ReadableStream = ReadableStream;
}
if (!globalThis.TransformStream) {
  globalThis.TransformStream = TransformStream;
}

// MessagePort mock
if (!globalThis.MessagePort) {
  globalThis.MessagePort = class MessagePort {
    postMessage() {}
    close() {}
    start() {}
  };
}

// Fetch API polyfills
if (!globalThis.fetch) {
  const { fetch, Headers, Request, Response } = require('undici');
  globalThis.fetch = fetch;
  globalThis.Headers = Headers;
  globalThis.Request = Request;
  globalThis.Response = Response;
}

// BroadcastChannel mock
if (typeof globalThis.BroadcastChannel === 'undefined') {
  globalThis.BroadcastChannel = class BroadcastChannel {
    constructor() {}
    postMessage() {}
    close() {}
  };
}

ポイント:

  • 実行順序: MSWをインポートするに実行される必要がある
  • 条件分岐: 既存のAPIを上書きしないよう条件チェック
  • undiciの使用: Node.js環境でのfetch APIを提供

3. Jest設定の更新

ファイル: jest.config.ts

import type { Config } from 'jest';
import nextJest from 'next/jest.js';

// MSWを動作させるための最低限のJest設定
const config: Config = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFiles: ['<rootDir>/jest.polyfills.js'], // 先にポリフィルを読み込む
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  testEnvironment: 'jsdom',
};

export default createJestConfig(config);

キーポイント:

  • setupFiles: テスト実行前のポリフィル読み込み
  • setupFilesAfterEnv: MSWサーバー設定(ポリフィル後)

4. 型安全なMSWハンドラーの作成

ファイル: src/mocks/handlers.ts

import { http, HttpResponse } from 'msw';
import { YourApiResponseType } from 'path/to/your/types';

// 型安全なモック
const mockApiResponse: Array<YourApiResponseType> = [
  {
    id: '1',
    name: 'Sample Item',
    // ...必要なフィールド
  },
  // ...他のデータ
];

export const handlers = [
  // 任意のAPIエンドポイントの例
  http.get('/api/example', () => {
    return HttpResponse.json(mockApiResponse);
  }),

  // 他のAPIエンドポイントも同様に追加可能
];

ポイント:

  • 一元管理: APIモックをハンドラーとして一元管理することで、テストコードの見通しが良くなります。
  • 型安全: TypeScriptの型をインポートして利用することで、モックデータと実際のAPIレスポンスの乖離を防ぎます。
  • 再利用性: ここで定義したハンドラーは、すべてのテストのベースとして再利用されます。

5. MSWサーバーの設定

ファイル: src/mocks/server.ts

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

ファイル: jest.setup.ts

import '@testing-library/jest-dom';

// MSWサーバーの設定
import { server } from './src/mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Next.js系のモック設定
jest.mock('next/router', () => ({
  useRouter: jest.fn(),
}));

// その他プロジェクトで必要なモックがあれば、必要に応じて追記

6. テストケースの実装

ファイル: src/components/__tests__/YourComponent.test.tsx

import { render, screen, waitFor } from '@testing-library/react';
import { YourComponent } from '../YourComponent';

// データ取得ライブラリのキャッシュを無効化するラッパー
describe('YourComponent', () => {
  it('should display data fetched from MSW', async () => {
    // 注意: コンポーネントがProvider(データ取得、テーマなど)を必要とする場合、
    // ここでラップするか、カスタムのrender関数を使用してください。
    render(<YourComponent />);

    // MSWから返されたモックデータが表示されるのを待つ
    await waitFor(() => {
      expect(screen.getByText('Sample Item')).toBeInTheDocument();
    });
  });

    // --- 例外ケース:データが空の場合 ---
  it('should display an empty message when no data is returned', async () => {
    // テストによって /api/exampleのレスポンスを上書きも可能
    server.use(
      http.get('/api/example', () => {
        return HttpResponse.json([]); // 空の配列を返す
      }),
    );

    render(<YourComponent />);
    await waitFor(() => {
      expect(screen.getByText('データがありません')).toBeInTheDocument();
    });
  });
});

ポイント:

  • waitFor: 非同期データ取得と、それに伴うUIの更新を待ち合わせます。
  • server.use(): 特定のテストケース内でのみ、ベースとなるハンドラーを一時的に上書きできます。これにより、エラーケースや空データの場合など、様々なシナリオを簡単にテストできます。

🔍 動作の仕組み

1. テストの実行フロー

2. なぜこのアプローチが優れているか

比較項目 従来のjest.spyOn(fetch)モック MSW (Mock Service Worker) アプローチ
リアルさ fetch関数自体を上書き ✅ ネットワーク層でリクエストを傍受
コード分離 ❌ テストコード内にモックが混在 ✅ モック定義をハンドラーとして完全に分離
保守性 ❌ テストごとにモックを再定義 ✅ ハンドラーを一元管理し、再利用が容易
型安全性 ⚠️ 手動でレスポンスを作成(型不一致リスク) ✅ アプリケーションと型定義を共有可能
デバッグ ❌ 実際のリクエスト内容が追いにくい ✅ 未処理リクエストの警告やエラー通知が可能
再利用性 ❌ Jestテスト環境に限定 ✅ 開発、テスト、Storybookで同じモックを共有可能

🎯 まとめ

MSWとJestを組み合わせることで、ネットワークレベルでのAPIモックテストが可能になり、以下のようなメリットがあります。

  • 信頼できるテスト: fetchを上書きするのではなく、ネットワークレベルでリクエストを傍受するため、より本番環境に近い信頼性の高いテストが可能
  • 高い保守性: APIのモックをハンドラーとして一元管理。テストコードから分離。
  • 優れた再利用性: 一度書いたモックは、テスト、Storybook、開発環境で使い回せる

最後まで読んでいただき、ありがとうございました!
この記事が、MSWの導入を迷っている方や、Jestとの連携方法を探している方へのヒントになれば嬉しいです。
いいねやコメント、フィードバックをいただけると、今後の執筆の励みになります! 🙌

📖 参考資料


執筆者: @hop2019_dev
個人開発: FanTrack

Discussion