React Testing Libraryを使ってみた(fetch編)

はじめに

今回はReact Testing Libraryでfetchをテストしてみます。

テスト

テスト対象コンポーネント

今回のテスト対象のコンポーネントです。
ボタンを押すとpropsで渡されたURLにfetchし、その内容を表示します。

import React from "react";
 
const Fetch = (props) => {
  const [json, setJson] = React.useState("");
 
  const handleClick = async () => {
    fetch(props.url)
    .then(response => response.json())
    .then((responseJson) => {
      setJson(JSON.stringify(responseJson));
    });
  };
 
  return (
    <div>
      <button onClick={handleClick}>button</button>
        {json}
    </div>
  );
};
 
export default Fetch;

テストコード

import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
 
import Fetch from "./Fetch";
 
const server = setupServer(
  rest.get("http://example.com/somethings/1", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ data: "something data" }));
  })
);
 
beforeAll(() => server.listen());
 
afterEach(() => {
  server.resetHandlers();
  cleanup();
});
 
afterAll(() => server.close());
 
describe("HTTPリクエスト", () => {
  it("【データ取得成功】[server]で設定したレスポンスが返ってくる事", async () => {
    render(<Fetch url={"http://example.com/somethings/1"} />);
    userEvent.click(screen.getByRole("button"));
 
    expect(await screen.findByText("{\"data\":\"something data\"}")).toBeInTheDocument();
  });
 
  it("【データ取得失敗】[server]で設定したレスポンスが返って来ない事", async () => {
    // テストケース内だけで使用するAPIのエンドポイントを設定する。
    server.use(
      rest.get("http://example.com/somethings/1", (req, res, ctx) => {
          return res(ctx.status(404));
        }
      )
    );
    render(<Fetch url={"http://example.com/somethings/1"} />);
    userEvent.click(screen.getByRole("button"));
    expect(screen.queryByText("something title")).toBeNull();
  });
});

テストで使用するAPIのエンドポイントを設定しています。

const server = setupServer(
  rest.get("http://example.com/somethings/1", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ data: "something data" }));
  })
);

beforeAllを使用すると、最初の1回だけ実行したい処理を記述できます。
ここでは上記で記述したserverを起動しています。

beforeAll(() => server.listen());

afterEachを使用すると、テストケース毎にテスト後に実行したい処理を記述できます。
server.resetHandlersはserverを毎回リセットします。
cleanupはrenderでマウントしたコンポーネントをアンマウントします。
今回の様なシンプルなケースでは無くても問題ありませんが、基本的には書いておく方が良いと思います。

afterEach(() => {
  server.resetHandlers();
  cleanup();
});

afterAllを使用すると、最後の1回だけ実行したい処理を記述できます。
serverを停止しています。

afterAll(() => server.close());

「await screen.findByText」を使用すると、fetchのレスポンスが返ってくるまで待つ事ができます。

expect(await screen.findByText("{\"data\":\"something data\"}")).toBeInTheDocument();

server.useを使用すると、テストケース内だけで使用するAPIのエンドポイントを設定できます。

server.use(
  rest.get("http://example.com/somethings/1", (req, res, ctx) => {
      return res(ctx.status(404));
    }
  )
);

APIのレスポンスが404を返すので、期待した値が表示されない事をテストしています。

expect(screen.queryByText("something data")).toBeNull();

補足

上記のテストコードだと処理時間が長い場合はエラーになってしまいます。
ここでは処理時間が長いfetchのテストについて記述します。

テスト対象コンポーネント

先程とほぼ同じですが、重い処理を想定して3秒待つ処理を入れています。

import React from "react";
 
const HeavyFetch = (props) => {
  const [json, setJson] = React.useState("");
 
  const handleClick = async () => {
    await new Promise(resolve => setTimeout(resolve, 3000)); // 重い処理を想定
    fetch(props.url)
    .then(response => response.json())
    .then((responseJson) => {
      setJson(JSON.stringify(responseJson));
    });
  };
 
  return (
    <div>
      <button onClick={handleClick}>button</button>
      {json}
    </div>
  );
};
 
export default HeavyFetch;

テストコード

前回のテストコードで実行すると、以下でエラーになると思います。
awaitで待つ時間が1秒なので、まだレスポンスが帰ってきていないのが原因です。

waitForで待機する事でタイムアウトを回避できます。

await waitFor(async () => {
  const result = await screen.findByText("{\"data\":\"something data\"}");
  expect(result).toBeInTheDocument();
}, {timeout: 5000});

さらに処理時間が長い場合はjestがタイムアウトするので、以下でタイムアウトを伸ばす事ができます。

jest.setTimeout(10000);