Claude Media
Claude CodeでMSWのAPIモックを書く — 実APIの応答から異常系とずれ検知まで

Claude CodeでMSWのAPIモックを書く — 実APIの応答から異常系とずれ検知まで

フロントのテストで外部APIをMSWに差し替える手順です。実APIの応答からhandlerを起こす指示、500と通信断の異常系、モックと実APIのずれをcontractテストで拾う構成を示します。

フロントエンドのテストで外部APIを呼ぶコードを検証するとき、fetch 自体を差し替えるモックをClaude Codeに書かせると、テストが実装の呼び方に縛られがちです。MSW(Mock Service Worker)は fetch ではなくネットワークの層で応答を差し替えるので、アプリのコードを一切変えずに済みます。

ここではVitestを使うフロントエンドを例に、MSWの土台づくり、実APIの応答からのhandler起こし、異常系の追加、実APIとのずれの検知までを順に作ります。Claude Codeに任せる指示文と、そのまま使える設定断片を併せて示します。

MSWは何を差し替えるのか

MSWは、Node.jsではリクエストモジュールを拡張して送信を横取りし、handlerに書いた応答を返すライブラリです。setupServer は、名前に「server」とあっても実際のサーバーは立てず、プロセスの中だけで動きます。

Claude Codeに書かせる対象は、次の3ファイルです。

  • src/mocks/handlers.ts: どのURLにどの応答を返すかを並べる
  • src/mocks/node.ts: handlerを setupServer に渡す
  • vitest.setup.ts: テストの前後で listen / resetHandlers / close を呼ぶ

handlerは msw/http の http と HttpResponse で書き、setupServer だけは msw/node から読み込みます。このimport元の違いは取り違えやすい箇所です。

土台をClaude Codeに作らせる

インストールは1行です。

npm install msw --save-dev

そのうえで、Claude Codeには次のように頼みます。Common workflowsのページが勧めるとおり、テストの依頼は「何を検証したいか」を具体的に書くほど既存のテストの流儀に揃います。

src/api/user.ts の getUser が呼ぶ https://api.example.com/users/:id を
MSW で差し替えたい。Vitest 用に handlers.ts / node.ts / vitest.setup.ts を
作り、vitest.config.ts の setupFiles に登録して。
最後に npx vitest run を実行して、出力を見せて。

最後の1行が効きます。ベストプラクティス集は、完了の根拠にはコマンドとその出力を求めるよう勧めています。「通りました」だけの報告では、読む側が確かめようがありません。

生成物は、例えば次のような形になります(MSWのクイックスタートに沿った例です)。

handlers.ts / node.ts / vitest.setup.ts の例
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw/http'
 
export const handlers = [
  http.get('https://api.example.com/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Taro Yamada',
      plan: 'pro',
    })
  }),
]
// src/mocks/node.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
 
export const server = setupServer(...handlers)
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './src/mocks/node'
 
beforeAll(() => server.listen({ onUnhandledFrame: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

afterEach の resetHandlers() は省かせません。テスト単位で足したhandlerを毎回消す役目で、これが無いと前のテストの異常系が次のテストに残ります。

CLAUDE.mdにモックの規約を置く

MSWまわりの約束は、会話のたびに言い直さずCLAUDE.mdに書きます。Memoryのページは、指示は「検証できる具体性」で書くよう勧めています。「テストを書くこと」より「npm test を実行すること」のような書き方です。

## APIモック(MSW)
- 外部APIの差し替えは MSW だけ。`vi.mock('fetch')` や axios のモックは書かない
- handler は `src/mocks/handlers.ts`、レスポンスの元データは `src/mocks/fixtures/` に置く
- 異常系はテスト内で `server.use(...)` を使い、handlers.ts の既定は成功系のみにする
- 「リクエストが呼ばれたこと」を expect しない。画面や戻り値の結果を検証する
- テスト実行は `npx vitest run`。結果の出力を貼って報告する

同じページは、CLAUDE.mdを1ファイル200行以下に収める目安も示しています。モックの細則が増えたら、.claude/rules/ に分けて、該当ファイルを触るときだけ読ませる手もあります。

実APIの応答からhandlerを起こす

手で書いた応答は、実APIの形から少しずつずれます。最初の成功系は、実際の応答を1回保存して、そこから起こさせます。

手順

実応答からfixtureを作る流れ

  1. 1

    応答を保存する

    開発用のキーで curl を打ち、レスポンスを src/mocks/fixtures/user.raw.json に保存します。キーは自分の端末の環境変数で渡し、プロンプトには貼りません。

  2. 2

    個人情報を置き換えさせる

    「氏名・メール・IDは架空の値に置き換えて user.json にして。キーの構造と型は変えない」と頼みます。user.raw.json はコミットしません。

  3. 3

    handlerをfixtureに繋ぐ

    handlers.tsは user.json を読み込んで返す形にします。応答の中身が増えてもhandlerの本体は変わりません。

curl -s https://api.example.com/users/abc-123 \
  -H "Authorization: Bearer $DEV_API_TOKEN" \
  -o src/mocks/fixtures/user.raw.json

fixtureにしておくと、後で述べるcontractテストが同じファイルを物差しに使えます。

異常系を足す: 500・401・通信断

成功系だけのテストは、アプリが失敗時に何を表示するかを一切守りません。MSWのドキュメントは、既定のhandlerを土台にしつつ、個々のテストで server.use() を使って応答を上書きする作りを勧めています。.use() で足したhandlerは初期のhandlerより優先されます。

Claude Codeには、「失敗時の画面の振る舞い」を先に決めて頼みます。

getUser の失敗時の振る舞いをテストして。
- 500: 「ユーザーを取得できませんでした」と再試行ボタンを出す
- 401: ログイン画面へ遷移する
- 通信断: 500 と同じ表示にする
それぞれ server.use で上書きし、成功系の handler は触らない。

テストは、例えば次の形になります。

import { http, HttpResponse } from 'msw/http'
import { server } from '../src/mocks/node'
import { renderUserCard } from './helpers'
 
test('500のとき再試行ボタンが出る', async () => {
  server.use(
    http.get('https://api.example.com/users/:id', () => {
      return HttpResponse.json({ message: 'boom' }, { status: 500 })
    }),
  )
  const view = await renderUserCard('abc-123')
  expect(view.getByText('ユーザーを取得できませんでした')).toBeTruthy()
  expect(view.getByRole('button', { name: '再試行' })).toBeTruthy()
})
 
test('通信断でも同じ表示になる', async () => {
  server.use(
    http.get('https://api.example.com/users/:id', () => {
      return HttpResponse.error()
    }),
  )
  const view = await renderUserCard('abc-123')
  expect(view.getByText('ユーザーを取得できませんでした')).toBeTruthy()
})

HttpResponse.error() はネットワークエラーを表す応答を作ります。その中身を細かく指定できない点も、ドキュメントに明記されています。クライアントごとにエラー文の扱いが違うためです。「通信断のときにエラー文言がこうなる」といったテストは書かないのが無難です。

「最初の1回だけ失敗して2回目で成功する」再試行のテストには、handlerの第3引数に { once: true } を付けます。一度使われるとhandlerは使用済みになり、以降のリクエストは初期のhandlerが受けます。

タイムアウトはどう再現するか

応答の遅れや無応答には、msw/utils/delay の delay を使います。MSWは既定では応答を遅らせないので、そのままではサーバーの遅延もタイムアウトも再現できません。delay には引数の違いで3通りの使い方があります。

  • await delay(1000): ミリ秒を指定して、その時間だけ応答を待たせる
  • await delay(): 実サーバー並みのランダムな遅延(約100〜400ms)を入れる。ただしNode.jsのテストでは、この引数なしの遅延は無効化される
  • await delay('infinite'): 応答を永遠に返さない

VitestはNode.jsで動くので、遅延を効かせたいときは時間か 'infinite' を明示します。タイムアウトの再現は後者です。

import { http } from 'msw/http'
import { delay } from 'msw/utils/delay'
 
server.use(
  http.get('https://api.example.com/users/:id', async () => {
    await delay('infinite') // このリクエストは保留されたままになる
    return new Response() // 到達しない
  }),
)

読み込み中の表示を確かめたいときは await delay(1000) を使い、応答が返る前のスピナーを検証します。どちらもアプリ側に AbortSignal.timeout などの打ち切りがあることが前提で、打ち切りのない fetch では 'infinite' のテスト自体が終わりません。Claude Codeにこのテストを書かせるときは、「先にアプリ側のタイムアウト実装を確認して、無ければ指摘して」と添えておきます。

「呼ばれたか」ではなく「どう振る舞うか」を検証する

Claude Codeにテストを書かせると、expect(handler).toHaveBeenCalledWith(...) のような、リクエストの中身を直接確かめるテストが出てきやすくなります。MSWのドキュメントは、このようなrequest assertionを既定では勧めないと明言しています。アプリの書かれ方を試すことになり、何をするかを試せないからです。

代わりに、リクエストの正しさはhandler側に書きます。ドキュメントの例では、ログインのhandlerがフォームに email が無ければ400を返します。アプリが正しいリクエストを送らなければ、画面の検証が落ちて問題に気づけます。

もう1つ、想定外の通信を検知する設定があります。server.listen({ onUnhandledFrame: 'error' }) は、handlerが無いリクエストをエラーにします。上の vitest.setup.ts に入れてあるのはこのためです。Claude Codeが新しいAPI呼び出しを足したのにhandlerを忘れたとき、実APIへ素通りせず、その場でテストが落ちます。

実APIとのずれをcontractテストで拾う

モックの弱点は、実APIが変わっても気づけないことです。fixtureと実APIの応答の「形」を比べるテストを別に1本置き、定期ジョブでだけ実行します。

ポイントは、MSWを入れない別のVitest設定で動かすことです。通常の設定に setupFiles があると、実APIへの通信も横取りされます。

// vitest.contract.config.ts
import { defineConfig } from 'vitest/config'
 
export default defineConfig({
  test: { include: ['test/contract/**/*.test.ts'] }, // setupFiles は書かない
})
// test/contract/user.contract.test.ts
import { describe, it, expect } from 'vitest'
import fixture from '../../src/mocks/fixtures/user.json'
 
const shape = (v: unknown): unknown =>
  Array.isArray(v) ? v.map(shape)
  : v && typeof v === 'object'
    ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, shape(x)]))
    : typeof v
 
describe.skipIf(!process.env.CONTRACT_API_TOKEN)('User API', () => {
  it('実APIの応答がfixtureと同じ形を保っている', async () => {
    const res = await fetch('https://api.example.com/users/abc-123', {
      headers: { Authorization: `Bearer ${process.env.CONTRACT_API_TOKEN}` },
    })
    expect(res.status).toBe(200)
    expect(shape(await res.json())).toEqual(shape(fixture))
  })
})
npx vitest run --config vitest.contract.config.ts

トークンが無い環境では skipIf で飛ばされ、通常のテストには影響しません。このテストが落ちたら、Claude Codeに「契約テストが失敗した。差分を見てfixtureとhandlerと型を直して」と頼みます。実APIを正としてモック側を直す流れです。

OpenAPIの仕様書があるAPIなら、仕様から型を生成する手もあります。生成コードを手で直させない設定はClaude CodeでOpenAPI Generatorを使うにまとめています。

つまずきやすい箇所

落とし穴

複数のsetupServerが同居して応答が食い違う

公式は、setupServer をテストごとに複数回呼ぶのは避け、1つのパターンを全体で貫くよう勧めています。Claude Codeが新しいテストファイルの先頭で独自に setupServer を作ると、グローバルの設定と競合します。CLAUDE.mdに「サーバーは src/mocks/node.ts の1つだけ」と書いておくと防げます。

  • ブラウザ専用APIをhandlerで使う: location や localStorage はNode.jsでは使えません。ドキュメントも、テスト環境側で用意する責任があると注意しています
  • フロントのE2Eには効かない: Node.jsの setupServer は、同じプロセス内のリクエストだけを差し替えます。実ブラウザを動かすE2Eでは別の仕組みが要ります。モバイルの場合はClaude CodeでMaestroのモバイルE2Eテストを書くが手がかりになります
  • バックエンド側のモック: サーバーサイドの非同期タスクを検証したいなら、Claude CodeでCeleryのタスクを書くのように、言語ごとのモック手法を使います

まとめ

MSWを使うと、Claude Codeに書かせるテストの焦点を「どう呼んだか」から「どう振る舞うか」へ移せます。

最初から全画面をMSW化する必要はありません。ステータスコードや読み込み中の表示で分岐が多い画面を1つ選び、異常系のテストを先に決めてから頼むと、書かれるテストの質が安定します。contractテストは、外部APIの仕様が頻繁に変わる連携や、モックの数が増えて手で追いきれなくなった時点で足せば、運用の手間に見合います。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. mswjs.io/docs/quick-start
  2. mswjs.io/docs/api/setup-server
  3. mswjs.io/docs/api/http-response
  4. mswjs.io/docs/api/delay
  5. mswjs.io/docs/best-practices/network-behavior-overrides
  6. mswjs.io/docs/best-practices/avoid-request-assertions
  7. code.claude.com/docs/en/common-workflows
  8. code.claude.com/docs/en/memory
  9. code.claude.com/docs/en/best-practices
この記事を共有:XはてブLinkedIn