⚡
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やその他機能との互換
⚠️ 解決した課題
-
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