19
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-30

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)時に以下のようなメッセージがブラウザのコンソールに表示されます。

msw1.png

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

msw2.png

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

msw3.png

感想

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

参考

19
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?