2児のパパ系Webエンジニア
の開発日記
A Dad-of-Two Engineer's Development Diary

MSWでAPIモックを入れたらフロント開発が止まらなくなった話!テストでの使い回しとハマりどころ

MSWでAPIモックを入れたらフロント開発が止まらなくなった話!テストでの使い回しとハマりどころ

🙌 結論から

バックエンドのAPIがまだ完成していないタイミングでフロントエンドを進めるとき、MSW(Mock Service Worker)を導入したらモック管理がかなり楽になりました!

以前は fetch をラップした自前のダミー関数でごまかしていたんですが、条件分岐が増えるたびに崩壊気味になっていて…(´・ω・`)

MSW はネットワークレイヤーでリクエストを横取りしてくれるので、実装コード側は本番とまったく同じ書き方のままモックに切り替えられます👍

💡 MSW とはなにか

MSW は Service Worker(ブラウザ)や Node.js のリクエストインターセプトの仕組みを使って、実際のネットワークリクエストをキャッチしモックレスポンスを返してくれるライブラリです。

fetch や axios の呼び出し側を一切変更しなくていいのが最大の特徴だと思うんですよね(´・∀・)

コンポーネント側は本物のAPIを叩いているつもりのまま、裏側だけモックに差し替わる感じですね(・∀・)

REST はもちろん GraphQL のリクエストにも対応していて、最近のプロジェクト構成ならだいたいカバーできます。

👀 導入してみた

インストールはシンプルです。

npm install msw --save-dev

ハンドラーはこんな感じで定義します。

import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'Taro' })
  }),
]

ブラウザ用のワーカースクリプトも1コマンドで生成できます。

npx msw init public/ --save

あとは開発サーバー起動時にワーカーを立ち上げるだけで、Network タブにも普通の通信として表示されます。

✨ 使ってみた感想

一番助かったのは、エラーレスポンスやローディング状態を自由に再現できることです。

500エラーや極端に遅いレスポンスって、実際のバックエンドが動いてる状態だとなかなか意図的に再現しづらくないですか(・_・;)

MSW ならハンドラーの中で delay() を挟んだり、ステータスコードを変えるだけで済みます。

http.get('/api/users/:id', async () => {
  await delay(2000)
  return HttpResponse.json({ message: 'Server Error' }, { status: 500 })
})

デザイナーへのレビュー依頼でも、バックエンド抜きでほぼ本番同様の画面を見せられるようになったのがだいぶ助かる・・!

🧪 テストでもハンドラーを使い回した

一通り導入して満足していたんですが、しばらくして気づいたのがテストコードとの相性の良さです。

Vitest や Jest のテスト環境でも、同じハンドラー定義をそのまま setupServer に渡せます。

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

export const server = setupServer(...handlers)

開発用に書いたモックがそのままテストの下地になるので、二重管理から解放されたのは正直かなり大きいです✨

コンポーネントテストで「このAPIが失敗したときの表示」を確認したいときも、その場でハンドラーを一時的に上書きするだけで済みました(^^)

🎲 GraphQLとFakerを組み合わせて使ってみた

私が実際に一番よく使っているのは、REST用のハンドラーとGraphQL用のハンドラーを同じ handlers.ts に共存させる書き方です。

MSWはGraphQL専用の graphql 名前空間も持っていて、REST APIとGraphQLのエンドポイントが混在しているプロジェクトでも、同じ仕組みでまとめて差し替えられます。

import { graphql, HttpResponse } from 'msw'

export const graphqlHandlers = [
  graphql.query('GetUser', ({ variables }) => {
    return HttpResponse.json({
      data: { user: { id: variables.id, name: 'Taro' } },
    })
  }),
]

もう一つ試して良かったのが、レスポンスをベタ書きせず @faker-js/faker でその場生成する方法です。

import { faker } from '@faker-js/faker'

http.get('/api/users', () => {
  const users = Array.from({ length: 10 }, () => ({
    id: faker.string.uuid(),
    name: faker.person.fullName(),
  }))
  return HttpResponse.json(users)
})

固定のJSONだと一覧画面の見た目チェックがすぐ飽きてくるんですが、Fakerでランダム生成にしてからは毎回違うデータで表示崩れに気づきやすくなったのが意外と便利でした。

TypeScriptのジェネリクスでリクエスト・レスポンスの型を縛れるのもMSW v2の強みなので、OpenAPIのスキーマから型を生成している場合は、ハンドラー側もその型で縛っておくとモックと実APIの形がズレたまま気づかない事故を防げます。

🤔 運用でハマりやすいポイント

MSW を入れてしばらく使うと、いくつかハマりどころが出てきます(・_・;)

  • モックが本物の API とずれていく: バックエンドの仕様が変わっても、ハンドラーは自動では変わらない
  • 本番のビルドに MSW が混ざる: 起動の条件を間違えると、本番でもモックが動いてしまう
  • ハンドラーのないリクエストに気づかない: モックし忘れた API が、そのまま本物に飛んでいく

1つ目は、OpenAPI の定義があるなら、そこから型やハンドラーを作ると、ずれに気づきやすくなります👀

2つ目と3つ目は、設定で防げます。

if (import.meta.env.DEV) {
  const { worker } = await import('./mocks/browser');
  await worker.start({ onUnhandledRequest: 'warn' });
}

import.meta.env.DEV の中で動的に読み込めば本番のビルドに入らず、onUnhandledRequest: 'warn' でモックし忘れに気づけます。

テストのときは 'error' にしておくと、モックし忘れたらテストが落ちるので、より安全です✨

パパ系Webエンジニア〜愛用ガジェット一覧 🛍️

この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌

👍 まとめ

MSW は、ネットワークリクエストをそのままの書き方でモックできるAPIモッキングライブラリです。

バックエンドの実装待ちで止まりがちなフロントエンド開発や、エラー状態のテストがしづらいという悩みには、かなり効果を実感できると思います🙌

まずは開発環境のモックから試してみて、慣れてきたらテストのセットアップにも広げていくのがおすすめです!

※ MSW のAPIやGraphQL対応状況は今も更新が続いています。最新情報は MSW公式ドキュメント をご確認ください👀

参考になったらシェアしてもらえるとうれしいです🙌

Xでポスト Threads はてブ LINE