msw(MockServiceWorker)とは?
- ブラウザ、Node環境でRest/Graphqlのリクエストをモックしてくれるライブラリです
- ローカルホストでモック用のサーバーを起動するのではなく、サービスワーカーレベルでリクエストをインターセプトしてリクエストを返却します
- StoryBookなど周辺ツールでも利用できます。また、今回はReactで使ってますが作るファイルにReact依存はないのでVue.jsなどでも使えます。
Reactプロジェクトにmswを導入する
まずはプロジェクト作成とmswをインストール
$ npx create-react-app msw-sample --template typescript
$ cd msw-sample
$ npm i -D msw
このとき作成されたpackage.json(抜粋)はこちら
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.1",
"@types/node": "^16.11.36",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"msw": "^0.41.0",
"react-scripts": "5.0.1"
}
msw用のディレクトリと必要なファイルを作成する
公式のReactサンプル を参考に以下のファイルを作成します。(公式はjsですがtsに変換してます)
/src
├── mocks
│ ├── handlers.ts
│ ├── browser.ts
│ └── server.ts
handlers.ts
handlers.tsはAPIの定義を記述するファイルです。今回は/api/usersにアクセスするとidとnameを持ったユーザーを3名返す処理を記述しています。
import { rest } from "msw";
export const handlers = [
rest.get("/api/users", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
name: "John",
},
{
id: 2,
name: "Alice",
},
{
id: 3,
name: "Bob",
},
])
);
}),
];
browser.ts
browser.tsはブラウザからアクセスした際にAPIをモックするためのファイルです。
import { setupWorker } from "msw";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
ServiceWokerとして動作させるため、以下のコマンドでファイルを生成します。成功した場合、mockServiceWoerker.jsというファイルがpublicフォルダ配下に作成されます。
$ npx msw init public --save
npm start
などdevelopmentモード時のみ起動するようindex.tsxに以下を追加します。
if (process.env.NODE_ENV === "development") {
const { worker } = require("./mocks/browser");
worker.start();
}
起動が成功するとブラウザ起動(npm start
)時に以下のようなメッセージがブラウザのコンソールに表示されます。
server.ts
server.tsはNode(主にテスト)で利用するためのファイルです。
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
こんな感じのAPIと通信してユーザー一覧を取得するカスタムフックがあった場合
import { useEffect, useState } from "react";
import axios from "axios";
export type User = {
id: number;
name: string;
};
export const useFetchUsers = () => {
const [data, setData] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchRequest();
}, []);
const fetchRequest = async () => {
setLoading(true);
try {
const res = await axios.get("/api/users");
setData(res.data);
setLoading(false);
setError(null);
} catch (e) {
console.log(e);
setLoading(false);
if (axios.isAxiosError(e)) {
setError(e.message);
}
}
};
return { data, loading, error };
};
テストファイルでは以下のように書きます。once()で通信エラーなどの一度限りの振る舞いを変えることも可能です。参考
※node-fetchを利用している場合は、node用にテストファイルにfetchのPolyfill(whatwg-fetchとか)を追加する必要があります。
import { server } from "../mocks/server";
import { renderHook, waitFor } from "@testing-library/react";
import { useFetchUsers } from "./useFetchUsers";
import { rest } from "msw";
describe("useFetchUsers.ts", () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("初期状態", () => {
const { result } = renderHook(() => useFetchUsers());
expect(result.current.data).toHaveLength(0);
expect(result.current.error).toBeNull();
});
test("API通信成功", async () => {
const { result } = renderHook(() => useFetchUsers());
await waitFor(() => xxx //通信終了を待つ
expect(result.current.data).xxx //成功時のテスト
});
test("API通信失敗(500エラー)", async () => {
server.use(
rest.get("/api/users", (req, res, ctx) => {
return res.once(ctx.status(500), ctx.json({ message: "Internal Server Error" }));
})
);
const { result } = renderHook(() => useFetchUsers());
await waitFor(() => xxx //通信終了を待つ
expect(result.current.data).xxx //失敗時のテスト
});
});
動かしてみる
App.tsxを以下のように修正します。
import React from "react";
import "./App.css";
import { useFetchUsers } from "./hooks/useFetchUsers";
function App() {
const { data, error, loading } = useFetchUsers();
if (loading) return <div>...loading</div>;
if (error) return <div>{error}</div>;
return (
<div className="App">
<header className="App-header">
{data.map((user) => {
return <div key={user.id}>{user.name}</div>;
})}
</header>
</div>
);
}
export default App;
npm start
でローカル実行してみましょう。以下のような画面が表示されればOKです。
コンソールにも通信成功のログが表示されます。
感想
- バックエンドの実装をまたずにフロントエンドの開発を進めたり、テストの実装がAPI通信処理をモックせずに(msw自体がモックですが)実装できるので積極的に採用していこうかと思います。また、プロジェクトではOpenAPIで記述することが多いので連携する方法も調べてみようと思います。