kokh log

主にフロントエンドの備忘録

React Testing Libraryの勉強メモ

React Testing Libraryについて勉強したので備忘録です。 ソースコードこちら

テストの種類

Unit Test, Integration Testの手法 → Jest + React Testing Library(EnzymeよりHooksとの相性がいい)

Jest

  • npm test
  • test case
  • expect

React Testing Library

  • render
  • userEvent
  • custom hook

CLI

Test Suites: テストが書かれたファイルの数
Tests: 書かれたテストの数

テストの名前を表示させるには
"test": "react-scripts test --env=jsdom --verbose"

UserEvent Test

it("Should trigger output function", async () => {
    const outputConsole = jest.fn();
    render(<RenderInput outputConsole={outputConsole} />);
    const inputValue = screen.getByPlaceholderText("Enter");
    await userEvent.type(inputValue, "test");
    await userEvent.click(screen.getByRole("button"));
    expect(outputConsole).toHaveBeenCalledTimes(1);
  });
  • jest.fn() でダミーのprops関数を定義し、その関数が何回呼ばれたかを評価する
  • useEvent はPromiseを返す(React18〜)
  • getByRole で使うroleは明示的に定義していなければhtmlタグとの相関はこのようになっている

API Mock Test

import { rest } from "msw";
import { setupServer } from "msw/node";
import MockServer from "./MockServer";

// 代用されるAPIの定義
const server = setupServer(
  rest.get("https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ username: "Bred dummy " }));
  })
);

// テストファイルの最初に1回実行される
beforeAll(() => server.listen());
// テストケースが終わる度に実行される
afterEach(() => {
  server.resetHandlers();
});
// テストファイルの最後に1回実行される
afterAll(() => {
  server.close();
});

describe("Mocking API", () => {
  it("[Fetch Success] データが正しくフェッチされてボタンがdisabledになる", async () => {
    render(<MockServer />);
    await userEvent.click(screen.getByRole("button"));
    expect(await screen.findByRole("heading")).toHaveTextContent("Bred dummy");
    expect(screen.getByRole("button")).toHaveAttribute("disabled");
  });

  it("[Fetch failure] エラーメッセージが表示されボタンが有効化される", async () => {
    // レスポンスを書き換える、このitの中でのみ有効
    server.use(
      rest.get(
        "https://jsonplaceholder.typicode.com/users/1",
        (req, res, ctx) => {
          return res(ctx.status(404));
        }
      )
    );
    render(<MockServer />);
    await userEvent.click(screen.getByRole("button"));
    expect(await screen.findByTestId("error")).toHaveTextContent(
      "Fetching Failed!"
    );
    expect(screen.queryByRole("heading")).toBeNull();
    expect(screen.getByRole("button")).not.toHaveAttribute("disabled");
  });
});
  • getBy, queryBy, findBy
    • 通常はgetBy
    • getByは対象がないとエラーになるため、対象が 存在しない ことを評価するためには queryBy
    • 非同期で対象が見つかるまで待機する場合はPromiseを返す findBy

Reducer Test

describe("increment action", () => {
    let initialState = {
      mode: 0,
      value: 1,
    };
    it("Should increment by 1 with mode 0", () => {
      const action = { type: increment.type };
      const state = reducer(initialState, action);
      expect(state.value).toEqual(2);
    });
    it("Should increment by 100 with mode 1", () => {
      initialState = {
        mode: 1,
        value: 1,
      };
      const action = { type: increment.type };
      const state = reducer(initialState, action);
      expect(state.value).toEqual(101);
    });
  });

Extra Reducer Test

const initialState = {
    mode: 0,
    value: 0,
  };
  it("Should output 100 + payload when fulfilled", () => {
    const action = { type: fetchDummy.fulfilled.type, payload: 5 };
    const state = reducer(initialState, action);
    expect(state.value).toEqual(105);
  });

Integration Test

let store;
  beforeEach(() => {
    store = configureStore({
      reducer: {
        customCounter: customCounterSlice,
      },
    });
  });

it("Should display value with incrementByAmount", async () => {
    render(
      <Provider store={store}>
        <Redux />
      </Provider>
    );
    await userEvent.type(screen.getByPlaceholderText("Enter"), "30");
    await userEvent.click(screen.getByText("Increment By Amount"));
    expect(screen.getByTestId("count-value")).toHaveTextContent(30);
  });

CustomHook Test

it("Should increment by 1", () => {
    const { result } = renderHook(() => useCounter(3));
    expect(result.current.count).toBe(3);
    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(4);
  });
  • renderHook はコンポーネント外でhookを実行できるようにするための関数
  • 内部でstateに変更を加える等のふるまいがある場合は act内で実行する

ふむふむ