Mock Service Worker(以下MSW)はHTTP通信をモックする開発用ツールです。フロントエンドでエラー表示やページネーションなどを作り込む時に、欲しいレスポンスがかんたんにシミュレートできて重宝します。
この記事では、AngularでMSWをセットアップする手順と、モックのカスタマイズをいくつか紹介します。
MSWには、サーバーサイドのNode.jsと、ブラウザのService Workerの2通りのセットアップ手順があり、この記事ではブラウザを対象としています。
-
@angular/cli
v18 -
msw
v2 -
Node.js
v22
インストール
既存プロジェクトにパッケージをインストールします。
npm i msw -D
initコマンド実行
CLIでコマンドを実行して mockServiceWorker.js
を追加します。
このファイルはブラウザ上のService Workerとして動作します。
npx msw init ./src --save
./src/
├── app
│ ├── app.component.html
│ ├── app.config.ts
│ └── ...
├── index.html
├── main.ts
├── mockServiceWorker.js # <=== これ
├── mocks
│ └── handlers.ts
ブラウザから参照できるよう angular.json
に登録します。
"architect": {
"build": {
"configurations": {
"development": {
"assets": [
+ "src/mockServiceWorker.js"
]
}
},
ハンドラの追加
HTTP通信をモックする「ハンドラ」用のファイルを追加します。
mkdir src/mocks
touch src/mocks/handlers.ts
ハンドラの中身は後述します。ここでは空で宣言しておきます。
export const handlers = [];
MSWを起動
ハンドラを呼び出すスクリプトを追加します。
touch src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
main.ts
を以下のように書き換えます。
import { isDevMode } from "@angular/core";
async function prepareApp() {
// ローカル開発環境に限定
if (isDevMode()) {
const { worker } = await import("./mocks/browser");
return worker.start();
}
return Promise.resolve();
}
// Angularアプリケーションを起動する直前にService Workerを立ち上げる
prepareApp().then(() => {
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
});
ここまでの手順で、Angularプロジェクトのディレクトリ構造は以下のようになります。
●印が付いているファイルはMSWのセットアップによって改変されたファイルです。
├── README.md
├── ● angular.json
├── node_modules/
├── ● package-lock.json
├── ● package.json
├── public/
├── src/
│ ├── app/
│ ├── index.html
│ ├── ● main.ts
│ ├── ● mockServiceWorker.js
│ ├── mocks/
│ │ ├── ● browser.ts
│ │ └── ● handlers.ts
│ └── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
MSWのGitHubでAngularアプリケーションのサンプルコードが提供されています。
公式ドキュメントで見当たらないものはこちらを参考にすると良さそうです。
https://github.com/mswjs/examples/with-angular
HTTP通信してみる
任意のコンポーネントにHTTP通信の処理を書いて、MSWのモックを試してみましょう。
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
+ provideHttpClient(),
]
};
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
@for (user of users(); track $index) {
<p>{{ user.name }}</p>
}
`,
})
export class AppComponent implements OnInit {
private readonly api = inject(HttpClient);
users = signal<User[]>([]);
ngOnInit() {
this.api.get<User[]>("/api/users").subscribe(users => {
this.users.set(users);
});
}
}
type User = {
id: number;
name: string;
};
さきほど空にしておいたハンドラを記述します。
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "Bob" },
{ id: 2, name: "Alice" },
]);
}),
];
ハンドラ宣言は以下のようなシンタックスです。
エンドポイントひとつ分を表します。
http.`{メソッド}`(`{URL}`, () => {
return HttpResponse.json(`{レスポンス}`);
});
エンドポイントを追加するには、配列にハンドラを追加します。
export const handlers = [
http.get("/api/users", () => { ... }),
http.get("/api/users/:id", () => { ... }),
http.post("/api/users", () => { ... }),
http.put("/api/users/:id", () => { ... }),
http.delete("/api/users/:id", () => { ... }),
];
Angularアプリケーションを起動してブラウザを確認してみましょう。
npm start
GET /api/users
に対して、モックのレスポンスが返ります。
モックのカスタマイズ
MSWの魅力は、モックのカスタマイズの容易さです。
イレギュラーを含むさまざまなケースで画面表示を作り込む時に活躍します。
- 巨大なリストが返るケース
- レスポンスが遅延するケース
- クエリ文字列でページネーション指定するケース
- パスパラメータに応じたデータを返すケース
- HTTP 500が発生するケース
- ポーリングするケース
通信用にバックエンドのアプリケーションのコードを改変したり、データベースにダミーデータを登録する必要はありません。TypeScriptで自由自在にモックが作れる便利さは偉大です。
またPOSTやDELETEをリクエストしてもデータが永続化されないので、ブラウザをリロードするだけで「最初からやり直し」ができます。
巨大なリストが返るケース
サイズの大きいリストを生成してレスポンスで返却します。
フィクスチャを宣言しても良いですし、TypeScriptで動的生成することもできます。
export const handlers = [
http.get("/api/users", () => {
const users = Array.from({ length: 10000 }).map((_, index) => {
return {
id: index + 1,
name: `User ${index + 1}`,
};
});
return HttpResponse.json(users);
}),
];
// GET /api/users
// [
// { id: 1, name: "User 1" },
// { id: 2, name: "User 2" },
// { id: 3, name: "User 3" },
// ...(10000件)...
// ]
レスポンスが遅延するケース
ユーティリティ関数 delay
を使います。
https://mswjs.io/docs/api/delay/
import { delay } from "msw";
export const handlers = [
http.get("/api/users", async () => {
await delay(10000);
return HttpResponse.json([{ id:1, name: "Bob" }]);
}),
];
// GET /api/users
// ...(10秒)...
// [{ id:1, name: "Bob" }]
クエリ文字列でページネーション指定するケース
コールバック関数の引数で request
オブジェクトを受け取り、クエリ文字列を取り出します。
https://mswjs.io/docs/recipes/query-parameters/
export const handlers = [
http.get("/api/users", ({ request }) => {
const users = [{}, {}, {}, ...];
const page = Number(new URL(request.url).searchParams.get("page"));
const perPage = 10;
const first = perPage * (page - 1);
const last = first + perPage;
return HttpResponse.json(users.slice(first, last));
}),
];
// GET /api/users?page=1
// [{}, {}, {}, ...]
パスパラメータに応じたデータを返すケース
コールバック関数の引数で params
オブジェクトを受け取り、パラメータを取り出します。
https://mswjs.io/docs/network-behavior/rest/#reading-path-parameters
export const handlers = [
http.get("/api/users/:id", ({ params }) => {
const { id } = params;
switch (Number(id)) {
case 1: return HttpResponse.json({ id: 1, name: "Bob" });
case 2: return HttpResponse.json({ id: 2, name: "Alice" });
default: ...
}
}),
];
// GET /api/users/1
// { id: 1, name: "Bob" }
// GET /api/users/2
// { id: 2, name: "Alice" }
HTTP 500が発生するケース
HttpResponse.json
のショートハンドを使わずに new HttpResponse()
すると、ステータスコードやレスポンスボディを指定できます。
https://mswjs.io/docs/api/http-response
export const handlers = [
http.get("/api/users", () => {
return new HttpResponse('Critical Error!', {
status: 500,
});
}),
];
// GET /api/users
// HTTP 500 Critical Error!
ポーリングするケース
ジェネレーター関数を使ってN回分のリクエストをシミュレートします。
ポーリング中にHTTP 500が発生するケースも試せますね。
https://mswjs.io/docs/recipes/polling
export const handlers = [
http.get("/api/users", function* () {
const list = [
{ id: 1, name: "Bob" },
{ id: 2, name: "Alice" },
{ id: 3, name: "John" },
];
let count = 0;
while (count < 3) {
yield HttpResponse.json(list.slice(0, count + 1));
count++;
}
return HttpResponse.json(list);
}),
];
// GET /api/users
// [{ id: 1, name: "Bob" }]
// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }]
// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }, { id: 3, name: "John" }]
fetch or XHR
https://mswjs.io/docs/limitations/
公式ドキュメントにはfetchをトリガしないHTTP通信について注意が記載されていますが、Angularの withFetch
を指定しても(fetch API)、指定しなくても(XMLHttpRequest)、特に変わりなくMSWが動作しました。
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()), // <===
]
};
Chrome 129.0.6668.101
Firefox 131.0.3
MSWの正常な稼働を確認するには、ブラウザのコンソールタブで [MSW] HH:mm:ss {URL}
のログが出力された事を確認します。
ログはHTTP通信のたびに吐き出されるため、初回確認の後はオフにしておくとコンソールが静かになります。
return worker.start({ quiet: true });
以上です。
かんたん便利にHTTP通信をモックできるMSW、気になった方はぜひ使ってみてください。