Mock Service Worker でフロントエンド自動テストのREST APIコールをモックする

# はじめに
この記事では、_**Mock Service Worker**_ というAPIをモックするライブラリを使って、フロントエンド自動テストのREST APIコールをモックする手順について記載していきます。

https://mswjs.io/

# 前提
開発環境は以下の通りです。
- Windows11
- VSCode
- TypeScript 5.0.2
- Node.js 18.15.5
- Express 4.18.2
- React 18.2.0
- Vite 4.2.0
- Vitest 0.29.8
- @testing-library/react 14.0.0

事前にテスト対象となるToDoアプリを作成しています。
概要は以下の通りです。

## Backend
Backend側では、ToDoリストを持っています。

```backend/server.ts
import express, { Express, Request, Response } from "express";
import { Todo } from "../types";
import cors from "cors";

const app: Express = express();
const port = 3001;

app.use(cors());

const sampleData = [
  { id: 1, title: "牛乳を買う", completed: false },
  { id: 2, title: "洗濯をする", completed: true },
  { id: 3, title: "請求書を支払う", completed: false },
];

let todoList: Todo[] = sampleData;

app.get("/todo-list", (req: Request, res: Response) => {
  res.json(todoList);
});

app.listen(port, () => {
  console.log(`listening on port ${port}`);
});
```

## Frontend
Frontend側で上記のToDoリストを取得します。

`frontend/src/api/index.ts` でAPIをコールするフックを定義します。
```frontend/src/api/index.ts
import useSWR from "swr";
import { Todo } from "../../../types/";

export const fetcher = async (path: string) => {
  try {
    const res = await fetch("http://localhost:3001" + path);
    return res.json();
  } catch (error) {
    console.error(error);
  }
};

export const useTodoList = () => useSWR<Todo[]>("/todo-list", fetcher);
```

そして、`frontend/src/routes/Home.tsx` で上記のフックを呼び出し、取得したToDoリストを表示します。

```frontend/src/routes/Home.tsx
import { FC } from "react";
import { useTodoList } from "../api";

export const Home: FC = () => {
  const { data: todoList, error, isLoading } = useTodoList();

  if (error) return <div>failed to load</div>;
  if (isLoading) return <div>loading...</div>;

  return (
    <div>
      <h1>ToDoリスト</h1>
      <ul>
        {todoList?.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              id={String(todo.id)}
              checked={todo.completed}
            />
            <label htmlFor={String(todo.id)}>{todo.title}</label>
          </li>
        ))}
      </ul>
    </div>
  );
};
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/824e8d43-3999-315b-0af9-e1f30c986888.png)

## Frontendの自動テスト
上記のFrontendの自動テストは以下のように実装しています。
この時点では、`Home` コンポーネントをレンダリングし、見出しが表示されていることをテストしています。

```__tests__/integration/routes/Home.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { Home } from "../../../routes/Home";

describe("Home Page test", () => {
  test("Render Home", async () => {
    render(<Home />);

    await waitFor(() => {
      screen.getByRole("heading", { name: "ToDoリスト" });
      screen.debug();
    });
  });
});
```

しかし、このテストを実行すると、テスト結果は成功となるものの、BackendのREST APIが動作しないため、フェッチエラーが発生してしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/c4889649-e482-1f03-2944-c7f5420ac712.png)
そのため、Mock Service Worker でREST APIのモックを作成します。

# 1. Mock Service Worker インストール
以下のコマンドでMock Service Worker をインストールします。
```
yarn add msw --dev
```

# 2. モックAPIの定義
まずはモックAPI定義用のファイル `src/__tests__/libs/mocks/handlers.ts` を作成します。
次に `msw`（Mock Service Worker）から `rest` をインポートし、モックを定義します。

```src/__tests__/libs/mocks/handlers.ts
import { rest } from "msw";

export const DUMMY_TODO_LIST = [
  { id: 1, title: "牛乳を買う", completed: false },
  { id: 2, title: "洗濯をする", completed: true },
  { id: 3, title: "請求書を支払う", completed: false },
];

export const handlers = [
  rest.get("http://localhost:3001/todo-list", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json(DUMMY_TODO_LIST));
  }),
];
```

`"http://localhost:3001/todo-list"` リクエストに対し、ToDoリストのデータを返すようにしています。レスポンスの引数はそれぞれ以下の通りです。
- `req`：リクエストに関する情報（今回は利用していません）
- `res`：モックレスポンスを作成するユーティリティ
- `ctx`：モックレスポンス内のステータスコードやヘッダー、ボディなどを設定する機能群
これでモックの定義はできました。

# 3. 利用設定
先ほど定義したモックAPIを自動テストで利用できるようにします。
まずは、モックサーバー設定用のファイル `src/__tests__/mocks/server.ts` を作成します。このファイル内でサーバーインスタンスを作成し、先ほど定義したモックを読み込みます。

```src/__tests__/libs/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handler";

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

次にモックサーバー設定用のファイルを自動テスト設定用ファイルで読み込みます。

```setup.ts
import matchers from "@testing-library/jest-dom/matchers";
import { cleanup } from "@testing-library/react";
import { afterEach, expect } from "vitest";
import { server } from "./mocks/server";

// extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers);

// establish API mocking before all tests
beforeAll(() => server.listen());

afterEach(() => {
  // reset any request handlers that we may add during the tests,
  // so they don't affect other tests
  server.resetHandlers();
  // runs a cleanup after each test case (e.g. clearing jsdom)
  cleanup();
});

// clean up after the tests are finished
afterAll(() => server.close());
```

# 4. テスト実行
これでテストを実行しても先ほどのフェッチエラーは発生しなくなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/05d2ae32-027e-72af-8471-2c65766e9c02.png)

また、APIレスポンスをモックしたので、レスポンス内容のテストも追加できます。
```__tests__/integration/routes/Home.test.tsx
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { Home } from "../../../routes/Home";
import { DUMMY_TODO_LIST } from "../../libs/dummyData";

describe("Home Page test", () => {
  test("Render Home", async () => {
    render(<Home />);

    await waitFor(() => {
      screen.getByRole("heading", { name: "ToDoリスト" });

      const todoList = screen.getAllByRole("listitem");
      expect(todoList).toHaveLength(DUMMY_TODO_LIST.length);
      todoList.forEach((todo, index) => {
        const DUMMY_TODO = DUMMY_TODO_LIST[index];
        expect(todo).toHaveTextContent(DUMMY_TODO.title);
      });
      screen.debug();
    });
  });
});
```

# 最後に
今回は　_**Mock Service Worker**_ を使って、フロントエンドテストのREST APIコールをモックする手順について記載しました。Mocke Service Worker は他にもたくさん機能があるので、今後扱っていければと思っています。

# 参考
- [Mock Service Worker](https://mswjs.io/)