Mock Service Worker(MSW):フロントエンドのAPIモック、バックエンド待ちよさようなら

Development tutorial - IT technology blog
Development tutorial - IT technology blog

すべてのフロントエンド開発者にとって身近な話

新機能を作っているのに、バックエンドのAPIがまだできていない、あるいはdevサーバーでAPIがエラーを起こしている。結果:ただ待つか、if (isDev) return fakeDataというコードをあちこちに書き散らして、デプロイ前に削除し忘れる。身に覚えがありませんか?

私も何度もそういう状況に陥ったことがあります。以前チームで、フロントエンドが3日早く完成したのにAPIが準備できておらず、何もテストできないというスプリントがありました。丸3日の無駄。そこで、もっとちゃんとした解決策を探し始めました――それがMock Service Worker(MSW)でした。

MSWとは何か、そして従来のモック手法と何が違うのか

MSWはJavaScriptライブラリで、HTTPリクエストをコード層ではなくネットワーク層でインターセプトできます。シンプルに聞こえますが、これがポイントです。

従来のAPIモック手法は、主に次のどちらかを行います:

  • コード内で直接モック:jest.mock('./api') ── テストでしか使えず、実際の開発中には使えない
  • フェイクサーバーを別途立てる(json-server、Expressモックなど) ── 追加のインストール、設定、メンテナンスが必要

MSWはまったく異なるアプローチを取ります。ブラウザにService Workerをインストールするか、テスト環境ではNode.jsインターセプターを使います。このWorkerがアプリと実際のネットワークの間に位置し、リクエストをキャッチして定義したロジックに基づくモックレスポンスを返します。

アプリは自分がモックデータを受け取っていることを知りません。通常通りfetch()を呼び出し、通常通りレスポンスを受け取ります。さらに重要なのは、同じハンドラーセットをdev環境でもテストスイートでも使えること――二度書く必要はありません。

MSWのインストールと設定

1. ライブラリのインストール

npm install msw --save-dev

2. ハンドラーの定義

ハンドラーは「アプリがエンドポイントXを呼び出したらYを返す」という宣言をする場所です。src/mocks/handlers.jsファイルを作成します:

import { http, HttpResponse } from 'msw'

export const handlers = [
  // GET /api/users — ダミーのユーザー一覧を返す
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: '山田太郎', role: 'admin' },
      { id: 2, name: '鈴木花子', role: 'user' },
    ])
  }),

  // POST /api/login — ログインのモック
  http.post('/api/login', async ({ request }) => {
    const body = await request.json()
    if (body.username === 'admin' && body.password === '1234') {
      return HttpResponse.json({ token: 'fake-jwt-token', success: true })
    }
    return HttpResponse.json(
      { message: 'ユーザー名またはパスワードが違います' },
      { status: 401 }
    )
  }),

  // GET /api/products/:id — 動的ルートパラメータ
  http.get('/api/products/:id', ({ params }) => {
    const { id } = params
    return HttpResponse.json({
      id,
      name: `商品 #${id}`,
      price: 99000,
    })
  }),
]

ハンドラーにJSONレスポンスを貼り付ける前に、私はよくtoolcraft.app/ja/tools/developer/json-formatterを使って素早くフォーマット・バリデーションしています――ブラウザに拡張機能をインストールするよりずっと便利です。

3. ブラウザ環境(devサーバー)のセットアップ

以下のコマンドを実行してMSWのService Workerファイルを作成します:

npx msw init public/ --save

このコマンドでpublic/mockServiceWorker.jsファイルが作成されます――これがブラウザで実際に動くWorkerスクリプトです。

src/mocks/browser.jsファイルを作成します:

import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

src/main.js(またはindex.js)で有効化します:

async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return
  const { worker } = await import('./mocks/browser')
  return worker.start()
}

enableMocking().then(() => {
  // ここにアプリをマウント
  ReactDOM.createRoot(document.getElementById('root')).render(<App />)
})

npm run devを実行してDevToolsを開くと、“[MSW] Mocking enabled”というログが表示されます。これ以降、ハンドラーにマッチするすべてのリクエストがインターセプトされ、インターネットへは一切出ていきません。

4. ユニットテスト(Vitest / Jest)のセットアップ

Node.jsにはService Workerがないため、MSWは専用のHTTPインターセプターを使います。src/mocks/server.jsファイルを作成します:

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

export const server = setupServer(...handlers)

Vitestのセットアップファイル(src/setupTests.js):

import { server } from './mocks/server'
import { afterAll, afterEach, beforeAll } from 'vitest'

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())  // 各テスト後にリセット
afterAll(() => server.close())

vite.config.jsに追記します:

test: {
  setupFiles: './src/setupTests.js',
}

MSWを使った実際のテストの書き方

MSWを使ったテストが実際どのようなものか見てみましょう。APIを呼び出すReactコンポーネントのテスト例です:

import { render, screen, waitFor } from '@testing-library/react'
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import UserList from './UserList'

test('ユーザー一覧を表示する', async () => {
  render(<UserList />)
  // データの読み込み完了を待つ
  await waitFor(() => {
    expect(screen.getByText('山田太郎')).toBeInTheDocument()
  })
})

test('APIが失敗した場合にエラーメッセージを表示する', async () => {
  // このテストのみハンドラーをオーバーライド
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json(
        { message: 'Internal Server Error' },
        { status: 500 }
      )
    })
  )
  render(<UserList />)
  await waitFor(() => {
    expect(screen.getByText(/エラーが発生しました/i)).toBeInTheDocument()
  })
})

2番目のテストケースではserver.use()を使って、そのテストのスコープ内だけハンドラーをオーバーライドしています。テスト終了後はafterEach(() => server.resetHandlers())が自動的にデフォルトのハンドラーに戻してくれます――手動でクリーンアップする必要はありません。

MSWをより効果的に使うためのコツ

ローディング状態をテストするためのレスポンス遅延

import { delay } from 'msw'

http.get('/api/users', async () => {
  await delay(1500)  // 1.5秒の遅延ネットワークをシミュレート
  return HttpResponse.json([...])
})

特定のリクエストを素通りさせるpassthrough

import { passthrough } from 'msw'

http.get('/api/public-data', () => {
  return passthrough()  // モックしない — リクエストをそのまま実サーバーに通す
})

機能別にハンドラーを分割する

// handlers/users.js
export const userHandlers = [http.get('/api/users', ...), http.post('/api/users', ...)]

// handlers/products.js
export const productHandlers = [http.get('/api/products', ...)]

// handlers/index.js
export const handlers = [...userHandlers, ...productHandlers]

この方法で、プロジェクトが大きくなってもハンドラーを見つけやすく、メンテナンスしやすくなります。

便利なツールから良い習慣へ

MSWを導入してから、テストを書く量が格段に増えました。理由はシンプルです:テストはもうサーバーが動いているかどうか、APIが利用可能かどうかに依存しません。マシンを起動してnpm testを実行するだけで、すぐに結果が得られます。

バックエンドがAPIコントラクトを変更した場合、ハンドラーを一箇所更新するだけで済みます。dev環境もテスト環境も自動的に更新されます。データ構造が変わったことでテストは通るのに実際のUIがエラーになる、という状況はもうありません。

MSWは実際のAPIとのインテグレーションテストを置き換えるものではありません――それは依然として必要です。しかし、私が日々直面している問題を正確に解決してくれます:開発が速くなり、テストが楽になり、フロントエンドとバックエンドがお互いを待たなくなる。両者の分業が明確なチームでは特に効果的です。

Share: