🚨

React + Testing Library + Jestの覚書

2022/05/18に公開

最近、Zenn に全然(?)記事書けてないなぁっていうのと、フロントエンドのテスト大事やなぁと感じることが多かったので、React + Testing Library + Jest の覚書を雑に書くことにした

(特定の用途で覚書まとめたら、この内容だったら Zenn にも出せるやんか、とかそんなことがあったわけでは断じてない)

JavaScript のテストフレームワークである Jest について

Jest での transformer の話

この transform を利用して実装されている JSX や TypeScript をトランスパイルしている

Jest の transformer には ts-jest や babel-jest、@swc/jest、esbuild-jest などがある

test.todo, test.each, test.only

Jest の test には便利なメソッドがある

  • test.todo()
    • このメソッドを使うことで、実装予定のテストを TODO として残せる
  • test.each()
    • 同じテストケースを異なるテストデータを使ってテストしたい場合に使える
    test.each([
      { title: "nus3", value: "3" },
      { title: "nus4", value: "4" },
      { title: "nus5", value: "5" },
    ])(
      "$title の場合", // ${name}でtest.eachで渡したオブジェクトのプロパティの値をここでも使える
      ({ test, value }) => {
        // test.eachオブジェクトの配列分ののテストが実行できる
      }
    );
    
  • test.only()
    • 一つのテストケースのみを実行したい場合に使える

Testing Library について

Testing Library は実際に対象のコンポーンエントをユーザーが操作してるようなテストを書くためのテストユーティリティ

コンポーネントのテスト方法

大まかに次の流れでコンポーネントのテストは

  1. 対象のコンポーネントを render する
  2. render したコンポーネントに対して、対象の要素を testing-library のクエリで取得する
  3. 対象の要素に対して何かしらのユーザー操作を行う(表示確認だけだったらこのステップは省略)
  4. 対象の要素が期待した状態になっているか確認する

実際に Testing Library のドキュメントに載ってるサンプルコードを少し編集して上記の流れを説明すると

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Login } from "."; // テストしたい対象のコンポーネント

test("should show login form", () => {
  render(<Login />); // 1. 対象のコンポーネントをrender

  const input = screen.getByLabelText("Username"); // 2. 対象の要素をクエリで取得
  userEvent.type(input, "nus3"); // 3. 対象の要素にユーザー操作を行う

  expect(input).toHaveValue("nus3"); // 4. 対象の要素が期待した状態になっているか確認
});

クエリの種類について

詳細は次のリンクの Summary Table を見ると何が違うのかわかる

https://testing-library.com/docs/queries/about/#types-of-queries

ざっくりまとめると

  • get 〇〇、query 〇〇、find 〇〇では、該当する要素がある時とない時で Error を返すかどうかが違う
  • 〇〇 by、〇〇 AllBy では該当の要素が複数あった場合に Error を返すかどうかが違う

クエリの優先順位について

Testing Library の基本原則では、テストコードはなるべくユーザーが操作するような実装を推奨しており、その基本原則に則ったクエリの優先度がある

https://testing-library.com/docs/queries/about/

  1. 対象の要素に誰も(マウス、支援技術、視覚的)がアクセスできるクエリ
    1. getByRole
    2. getByLabelText
    3. getByPlaceholderText
    4. getByText
    5. getByDisplayValue
  2. セマンティッククエリ
    1. getByAltText
    2. getByTitle
  3. テスト ID
    1. getByTestig

@testing-library/jest-dom について

@testing-library/jest-dom を使用すると、テスト時のコンポーネント状態を確認するための便利なメソッド(カスタムマッチャー)が使えるようになる

どのようなカスタムマッチャーがあるかは次の README を読むとイメージできる

https://github.com/testing-library/jest-dom

たとえば

  • 対象の要素が document ないにあるかどうかを確認する toBeInTheDocument()
  • 対象の要素が visible かどうかを確認するtoBeVisible()
  • 対象の要素に該当の属性があるかどうかを確認するtoHaveAttribute(attr: string, value?: any)
  • 対象の要素がフォーカスされているかどうかを確認するtoHaveFocus()

などがある。

対象の要素の状態をテストしたい時に、一度この jest-dom にどんなカスタムマッチャーがあるのか確認しても良いかもしれない

waitFor について

テスト対象のコンポーネントの表示やユーザー操作の中で非同期の実装がある場合に、await + waitFor を使うことで非同期処理を待ってくれる(waitFor に渡した callback 関数がエラーを投げなくなるまで待つようになる)

実際に waitFor の実装例を Testing Library のドキュメントのサンプルコードを参考に作ってみると次のようになる

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Login } from ".";
import * as loginApi from "./api";

test("should submit login form", async () => {
  const mockLoginApi = jest.fn();
  jest.spyOn(loginApi, "login").mockImplementation(mockLoginApi); // submitボタンを押された際に実行されるloginのモック

  render(<Login />);

  const submitButton = screen.getByRole("button");
  userEvent.click(submitButton);

  // loginApiのloginが呼ばれるまで待つ
  await waitFor(() => expect(mockLoginAPI).toHaveBeenCalledTimes(1));

  // loginのAPIが呼ばれた後のコンポーネントの状態をテストできる
});

logDOM について

logDOM という関数が@testing-library/react にはあり、テスト時に実際にクエリで取得した要素を確認することができる

import { logDOM, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Login } from ".";

test("should show login form", () => {
  render(<Login />);

  const input = screen.getByLabelText("Username");
  // test実行時にscreen.getByLabelText('Username')で取得したDOMが出力される
  logDOM(input);
});

userEvent について

GitHubで編集を提案

Discussion