Mock Service Worker によるテストでのシームレスな API モッキング
Min-jun Kim
Dev Intern · Leapcell

はじめに
現代のウェブ開発の世界では、アプリケーションはデータを取得し、操作を実行し、動的なユーザーエクスペリエンスを提供するために、外部 API に頻繁に依存しています。この相互接続性は強力ですが、テストに関しては重大な課題を提示します。テスト実行中に遅い、信頼できない、あるいは利用不可能な API に依存するコンポーネントをどのようにテストしますか? API のデータが変更される可能性がある場合に、一貫したテスト結果をどのように保証しますか? 従来、開発者は専用のテストサーバー、扱いにくいスタブライブラリ、あるいは fetch
や XMLHttpRequest
を直接モックするといった複雑なセットアップに頼ることがありましたが、これはしばしば脆弱なテストと高いメンテナンスコストにつながっていました。
ここで、Mock Service Worker (MSW) のような強力なツールの出番です。MSW は、ネットワークレベルで直接 API リクエストをインターセプトおよびモックするためのエレガントで堅牢なソリューションを提供し、テストに比類のない制御と信頼性をもたらします。アプリケーションコードに触れることなく実際の API の動作をシミュレートできる能力は、回復力があり効率的なテストスイートを構築するための不可欠な資産となります。この記事では、MSW の核心原則、その仕組み、そして JavaScript テスト戦略を強化するために MSW をどのように活用できるかについて深く掘り下げていきます。
コアコンセプトの理解
実践的な実装に入る前に、関連する主要な用語について共通の理解を深めましょう。
- API (Application Programming Interface): 異なるソフトウェアアプリケーションが互いに通信できるようにする定義済みのルールのセット。ウェブ開発では、これは通常、データ交換のための HTTP ベースの通信を指します。
- モッキング (Mocking): テストにおいて、モッキングとは、コードが対話する依存関係(API など)のシミュレートされたバージョンを作成することを含みます。目標は、テスト対象のユニットを外部依存関係から分離し、テストがユニットのロジックのみに焦点を当てることを保証することです。
- Service Worker: ブラウザのメイン実行スレッドとは別に、バックグラウンドで実行される JavaScript ファイル。Service Worker は、ネットワークリクエストをインターセプトしたり、リソースをキャッシュしたり、プッシュ通知を処理したりできます。MSW は、このブラウザ機能を巧妙にモッキング機能に使用しています。
- ネットワークリクエストインターセプト (Network Request Interception): ネットワークリクエスト(例: HTTP
GET
、POST
、PUT
、DELETE
)が元の宛先に到達する前に、それをキャプチャして操作する能力。MSW は Service Worker を介してこれを実現します。 - 単体テスト (Unit Testing): アプリケーションの個々のコンポーネントまたは関数を分離してテストすること。
- 統合テスト (Integration Testing): アプリケーションのさまざまな部分が、モックまたは実際の API との対話を含めて、どのように連携して統合されたユニットとして機能するかをテストすること。
MSW の仕組み:ネットワークレベルでのインターセプト
MSW の革新性は、Service Worker API の活用にあります。fetch
や XMLHttpRequest
のようなグローバルオブジェクトをパッチする従来のモックライブラリ(競合状態やフレームワーク固有の問題を起こしやすい)とは異なり、MSW はネットワークレベルで動作します。
MSW をセットアップすると、次のようになります。
- Service Worker の登録: MSW は、ブラウザ(または Node.js 環境)に Service Worker を登録します。
- リクエストインターセプト: この Service Worker は、アプリケーションから発信されるすべてのネットワークリクエストのプロキシとして機能します。
- ハンドラーのマッチング: MSW がインターセプトすべき URL と HTTP メソッドを指定する「リクエストハンドラー」を定義します。リクエストが定義されたハンドラーに一致すると、MSW はそれをインターセプトします。
- モックレスポンス: リクエストを実際の API に進める代わりに、MSW はハンドラーの定義に従って調整された洗練されたモックレスポンスを返します。このレスポンスには、カスタムステータスコード、ヘッダー、および JSON ボディを含めることができます。
- 透過的な操作: アプリケーションの観点からは、あたかも実際の API と通信しているかのように見えます。アプリケーションコードは、リクエストがインターセプトおよびモックされていることを認識する必要はありません。
このアプローチは、いくつかの重要な利点を提供します。
- 真の分離: テストは、外部 API の可用性やデータの変動から独立します。
- フレームワーク非依存: MSW は、ネットワークレイヤーで動作するため、アプリケーションレイヤーではなく、任意の HTTP クライアントライブラリ (
fetch
、axios
、XMLHttpRequest
) および任意の JavaScript フレームワーク (React, Vue, Angular など) で機能します。 - 現実的な動作: ネットワークエラー、遅延、複雑なデータ構造をシミュレートでき、テストをより堅牢にできます。
- テストの不安定性の軽減: 一貫したモックレスポンスは、信頼性が高く再現可能なテスト結果につながります。
テストでの実践的な実装
API からデータを取得するシンプルな React コンポーネントを例に、典型的なテストシナリオで MSW を使用する方法を説明します。テスト環境には Jest と React Testing Library を使用します。
1. インストール
まず、MSW および必要なテストライブラリをインストールします。
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @mswjs/http-middleware msw # または yarn add -D jest @testing-library/react @testing-library/jest-dom @mswjs/http-middleware msw
2. リクエストハンドラーの定義
モック API レスポンスを定義するために、src/mocks/handlers.js
のようなファイルを作成します。
// src/mocks/handlers.js import { http, HttpResponse } from 'msw'; export const handlers = [ // /users への GET リクエストをモック http.get('https://api.example.com/users', () => { return HttpResponse.json([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ], { status: 200 }); // 200 OK レスポンスをシミュレート }), // /posts への POST リクエストをモック http.post('https://api.example.com/posts', async ({ request }) => { const newPost = await request.json(); console.log('Received new post:', newPost); // リクエストボディを検査できます return HttpResponse.json({ id: 99, ...newPost }, { status: 201 }); // 201 Created レスポンスをシミュレート }), // パラメータ付き GET リクエストをモック http.get('https://api.example.com/users/:id', ({ params }) => { const { id } = params; if (id === '1') { return HttpResponse.json({ id: 1, name: 'Alice' }, { status: 200 }); } return HttpResponse.json({}, { status: 404 }); // 404 Not Found をシミュレート }), ];
3. テスト用の msw セットアップ
Jest のような Node.js 環境向けに MSW を初期化するために、src/mocks/server.js
のようなセットアップファイルを作成します。
// src/mocks/server.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; // 指定されたリクエストハンドラーでリクエストモックサーバーを構成します。 export const server = setupServer(...handlers);
次に、src/setupTests.js
(またはテスト環境を構成する場所) で Jest にこのセットアップを使用するように構成します。
// src/setupTests.js (またはテスト環境を構成する場所) import '@testing-library/jest-dom'; import { server } from './mocks/server.js'; // すべてのテストの前に API モックを確立します。 beforeAll(() => server.listen()); // テスト内で宣言されているリクエストハンドラーをリセットします (一時的なリクエスト用)。 // これにより、テスト間でクリーンなテスト状態が維持されます。 afterEach(() => server.resetHandlers()); // テスト終了後にクリーンアップします。 afterAll(() => server.close());
package.json
または jest.config.js
で Jest の設定に src/setupTests.js
が含まれていることを確認してください。
// package.json { "jest": { "setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"] } }
4. テスト対象のコンポーネントの作成
ユーザーを取得する UserList.js
コンポーネントがあると仮定します。
// src/components/UserList.jsx import React, { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch('https://api.example.com/users') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { setUsers(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, []); if (loading) { return <div>Loading users...</div>; } if (error) { return <div>Error: {error.message}</div>; } return ( <div> <h1>User List</h1> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); } export default UserList;
5. テストの作成
次に、React Testing Library を使用して UserList.js
のテストを作成します。
// src/components/UserList.test.jsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import UserList from './UserList'; import { server } from '../mocks/server'; import { http, HttpResponse } from 'msw'; describe('UserList component', () => { it('displays user names fetched from the API', async () => { render(<UserList />); expect(screen.getByText(/loading users/i)).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); expect(screen.getByText('Bob')).toBeInTheDocument(); expect(screen.queryByText(/loading users/i)).not.toBeInTheDocument(); }); it('displays an error message when API fetch fails', async () => { // この特定のテストケースのデフォルトハンドラーをオーバーライドします。 server.use( http.get('https://api.example.com/users', () => { return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }); }) ); render(<UserList />); await waitFor(() => { expect(screen.getByText(/error: network response was not ok/i)).toBeInTheDocument(); }); expect(screen.queryByText('Alice')).not.toBeInTheDocument(); }); it('displays a single user when fetching by ID', async () => { // この例は、単一ユーザーを取得するコンポーネントを想定していますが、 // パラメータ付きモックの使用法を示しています。 // UserList では、通常、単一ユーザー表示用の別のコンポーネントがあります。 // しかし、ハンドラーの使用法を示すために: server.use( http.get('https://api.example.com/users/1', () => { return HttpResponse.json({ id: 1, name: 'Alice Smith' }, { status: 200 }); }) ); // UserList が特定のユーザーを取得するためのプロップを受け取れる場合: // render(<UserList userId={1} />); // await waitFor(() => { // expect(screen.getByText('Alice Smith')).toBeInTheDocument(); // }); // この UserList では、他の ID のネガティブパスのみをテストします。 server.use( http.get('https://api.example.com/users/99', () => { return HttpResponse.json({}, { status: 404 }); }) ); // ユーザー 99 を取得するコンポーネントがあった場合、404 の動作が表示されます。 }); });
server.use()
を使用することで、特定のテストケースに対して特定のハンドラーをオーバーライドでき、アプリケーションコードやグローバルモックを変更せずにさまざまな API レスポンス(成功、エラー、空データ)をテストできることに注意してください。afterEach
の resetHandlers()
は、これらのオーバーライドが後続のテストに漏洩しないことを保証します。
アプリケーションシナリオ
MSW の汎用性は、さまざまなテストシナリオに適しています。
- 単体テストと統合テスト: 示されたように、API と対話する UI コンポーネントのテストに最適であり、さまざまなデータ状態で正しくレンダリングされることを保証します。
- Storybook コンポーネント開発: MSW を Storybook と統合して、コンポーネントに現実的な静的データを提供し、デザイナーと開発者がライブバックエンドなしでさまざまな API 状態のコンポーネントと対話できるようにします。
- エンドツーエンドテスト (Cypress, Playwright, Selenium): E2E テストは通常、実際のバックエンドにヒットしますが、MSW は、特に初期開発中や問題のある外部サービスの場合、機能の迅速なプロトタイピングや特定 E2E シナリオの一貫したデータ確保に強力なツールとなり得ます。
- ホットモジュールリロードによるローカル開発: MSW は、Vite や Webpack Dev Server のようなツールを使用してブラウザでローカル開発中に使用することもできます。これにより、バックエンド API がまだ開発中または利用できない場合でも、開発者はフロントエンド機能に取り組むことができ、一貫したモックデータを提供できます。
結論
Mock Service Worker は、JavaScript アプリケーションでの API モッキングへのアプローチを根本的に変えます。ネットワークレベルで動作することにより、従来のモック手法にありがちな一般的な落とし穴を排除し、API 対話をシミュレートするための堅牢でフレームワークに依存しない、驚くほど透明性の高い方法を提供します。これにより、より信頼性が高く、保守しやすく、効率的なテストが可能になり、最終的には開発者がより高品質なアプリケーションに自信を持って構築できるようになります。MSW は、テストスイートを後戻りや予期しない動作に対する信頼できる防御策にするために、フロントエンドテストをバックエンドから分離することを真に可能にします。