ReactのAPI通信をmswでモックする

## msw(MockServiceWorker)とは？

- ブラウザ、Node環境でRest/Graphqlのリクエストをモックしてくれるライブラリです
    - [公式HP](https://mswjs.io/)
- ローカルホストでモック用のサーバーを起動するのではなく、サービスワーカーレベルでリクエストをインターセプトしてリクエストを返却します
- StoryBookなど周辺ツールでも利用できます。また、今回はReactで使ってますが作るファイルにReact依存はないのでVue.jsなどでも使えます。

## Reactプロジェクトにmswを導入する

まずはプロジェクト作成とmswをインストール

```bash
$ npx create-react-app msw-sample --template typescript
$ cd msw-sample
$ npm i -D msw
```

このとき作成されたpackage.json(抜粋)はこちら

```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サンプル](https://github.com/mswjs/examples/tree/master/examples/rest-react) を参考に以下のファイルを作成します。(公式はjsですがtsに変換してます)

```bash
/src
├── mocks
│   ├── handlers.ts
│   ├── browser.ts
│   └── server.ts
```

### handlers.ts

handlers.tsはAPIの定義を記述するファイルです。今回は/api/usersにアクセスするとidとnameを持ったユーザーを3名返す処理を記述しています。

```tsx
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をモックするためのファイルです。

```tsx
import { setupWorker } from "msw";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
```

ServiceWokerとして動作させるため、以下のコマンドでファイルを生成します。成功した場合、mockServiceWoerker.jsというファイルがpublicフォルダ配下に作成されます。

```tsx
$ npx msw init public --save
```

`npm start` などdevelopmentモード時のみ起動するようindex.tsxに以下を追加します。

```tsx
if (process.env.NODE_ENV === "development") {
  const { worker } = require("./mocks/browser");
  worker.start();
}
```

起動が成功するとブラウザ起動(`npm start`)時に以下のようなメッセージがブラウザのコンソールに表示されます。

![msw1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277523/746fd911-810e-0a01-238d-077f0862b3e1.png)

### server.ts

server.tsはNode(主にテスト)で利用するためのファイルです。

```tsx
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
```

こんな感じのAPIと通信してユーザー一覧を取得するカスタムフックがあった場合

```tsx
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()で通信エラーなどの一度限りの振る舞いを変えることも可能です。[参考](https://mswjs.io/docs/api/setup-server/use#one-time-override)

※node-fetchを利用している場合は、node用にテストファイルにfetchのPolyfill(whatwg-fetchとか)を追加する必要があります。

```tsx
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を以下のように修正します。

```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です。

![msw2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277523/682a75e8-81ad-44e3-124a-ac0731a792ce.png)


コンソールにも通信成功のログが表示されます。

![msw3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277523/c1e7cfb8-9294-8127-bce7-f5d503633a43.png)


## 感想

- バックエンドの実装をまたずにフロントエンドの開発を進めたり、テストの実装がAPI通信処理をモックせずに（msw自体がモックですが）実装できるので積極的に採用していこうかと思います。また、プロジェクトではOpenAPIで記述することが多いので連携する方法も調べてみようと思います。

## 参考

- [公式ドキュメント](https://mswjs.io/docs/)
- [フロントエンドのテストのモックには msw を使うのが最近の流行りらしい](https://zenn.dev/azukiazusa/articles/using-msw-to-mock-frontend-tests)