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
応答を保存する
開発用のキーで
curlを打ち、レスポンスをsrc/mocks/fixtures/user.raw.jsonに保存します。キーは自分の端末の環境変数で渡し、プロンプトには貼りません。 - 2
個人情報を置き換えさせる
「氏名・メール・IDは架空の値に置き換えて
user.jsonにして。キーの構造と型は変えない」と頼みます。user.raw.jsonはコミットしません。 - 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.jsonfixtureにしておくと、後で述べる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の仕様が頻繁に変わる連携や、モックの数が増えて手で追いきれなくなった時点で足せば、運用の手間に見合います。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- mswjs.io/docs/quick-start
- mswjs.io/docs/api/setup-server
- mswjs.io/docs/api/http-response
- mswjs.io/docs/api/delay
- mswjs.io/docs/best-practices/network-behavior-overrides
- mswjs.io/docs/best-practices/avoid-request-assertions
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/memory
- code.claude.com/docs/en/best-practices