20251027
MSW
Mock Service Workerの略。
ブラウザの Service Worker 機能を使ったAPIモックツール。ブラウザの fetch を横取りしてテスト用のデータを返す。
Service Worker が無い Node.js では http.ClientRequest 系のリクエストをインターセプトするらしい。これによって、RSCなどサーバーからのリクエストを行うケースでもリクエストを横取りできる。
※ net モジュールを直接使うリクエスト(undici など)のインターセプトはできないらしい。
https://mswjs.io/docs/limitations/
インストールの概要
仕組み上、別でAPIサーバーを用意するのではなく、アプリケーションに Service Worker や Node.js 用の処理を組み込むことになる。
- Service Worker 用の設定(ブラウザ側のfetch)
- Service Worker スクリプトの設置
npx msw init <PUBLIC_DIR> --saveで自動生成できる
- Service Worker を登録するためのスクリプト追加
- Service Worker スクリプトの設置
- Node.js 用の設定(サーバー側のfetch)
- サーバーのセットアップ処理追加
- リクエストハンドラの設定
- 特定のリクエストに対するレスポンスをモック化する処理の追加
Next.js での使用の注意点
Next.js ではサーバー側、クライアント側ともに諸々隠蔽されているのでさらに設定が必要。
サーバー側では Node.js の挙動をいじるために instrumentation という機能を使用する。
https://nextjs.org/docs/15/app/guides/instrumentation
instrumentation.ts は サーバープロセスが起動するときに一度だけ呼び出される。MSWを使用する際はここでサーバーの挙動を上書きする。
クライアント側では初回のレンダリング前に ServiceWorker の設定を行いたいため、 コンテンツの親コンポーネントを新たに追加し、ServiceWorkerの準備が整ってからコンテンツを表示させる仕組みにする。
※ instrumentation-client.ts でもいいがうまく動かないケースがあるとのこと
https://dev.to/ajth-in/mock-client-side-server-side-api-requests-using-nextjs-and-mswjs-9f1
リクエストハンドラの設定
// lib/msw/handlers/index.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("APIのURL", () => {
return HttpResponse.json({
message: "Hello, world!",
});
}),
];モックしたいURLとモックデータを記述する。これを後で MSW の setupWorker や setupServer に渡す。
Node.js 用の設定(サーバー側の設定)
// lib/msw/setup/server.ts
import { setupServer } from "msw/node";
import { handlers } from "../handlers";
export const server = setupServer(...handlers);// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { server } = await import("./lib/msw/setup/server");
server.listen();
}
}先ほど作成したリクエストハンドラーを setupServer に渡してサーバー設定を構築し、 instrumentation.ts でサーバーに適用する。
server.listen() でサーバーを起動しているように見えるが、実際はすでに起動している開発サーバーに対してリクエストを横取りするパッチを当てている。
Service Worker 用の設定(クライアント側の設定)
以下コマンドで自動的に Service Worker のスクリプトが生成される。 <PUBLIC_DIR> で指定した場所に設置されるため、ドキュメントルートで問題ない。
npx msw init <PUBLIC_DIR> --saveこのファイルはそのまま使えるので手を加える必要はない。
// lib/msw/setup/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "../handlers";
export const worker = setupWorker(...handlers);こちらはリクエストハンドラーを setupWorker に渡して ServiceWorker との連携設定を構築。(仕組みとしては ServiceWorkerで横取り → クライアントにモックデータを確認 → ServiceWorkerからモックデータをレスポンス、と言う流れっぽい?)
設定の反映を初回レンダリング前に行う必要があるため、それ用のコンポーネントを作成する。
// lib/msw/provider/index.tsx
"use client";
import { ReactNode, useEffect, useState } from "react"
export default function MswProvider({children}: {children: ReactNode}) {
const [mockingEnabled, enableMocking] = useState(false);
useEffect(() => {
async function mock() {
const { worker } = await import('../setup/browser');
await worker.start();
enableMocking(true);
}
mock();
}, []);
if (!mockingEnabled) {
return null;
}
return <>{children}</>
}ServiceWorkerの諸々が終わってから子コンポーネントが表示されるようにしている。
// app/layout.tsx
import { ReactNode } from "react";
import MswProvider from '@/lib/msw/provider';
export default function RootLayout({children}: {children: ReactNode}) {
return (
<html lang="ja">
<body>
<MswProvider>
{ children }
</MswProvider>
</body>
</html>
)
}layout.tsx にて先ほどのコンポーネントでコンテンツ部分をラップ。
本番環境用の設定
サーバー設定やServiceWorker用の設定は本番環境では除外したいのでその対応も必要になる。
調べきれてないのでまた調べる。