🙆‍♀️

✅RTL(React Testing Library)、ちゃんと理解して使おうか。

2023/03/15に公開

まえがき

今いる開発現場ではReactを採用しており、方針として「テストちゃんと書こうぜ(´_ゝ`)」という感じなので、よくテストコードを書いたりレビューする機会は多い。

しかし、Reactのテストで欠かせない要素であるReact Testing Library, Enzymeについて正直ちゃんと理解できていない/他者に説明できる自信がない/雰囲気でやってる感否めない(´_ゝ`)ので、本稿で克服する。

インストール

npm install --save-dev @testing-library/react
// toBeInTheDocument使いたい
npm install @testing-library/jest-dom  --save-dev;

RTLなし/あり でどんなコードになるのか?

React公式が公開しているテストのレシピ集はRTLを使っていないソースコードなので、これを題材に分析する。ソースコードと詳細はリンク先を見てもらった方がわかりやすいので、ここでは分析結果をまとめる。

01. 土台となるdiv要素の生成&ReactコンポーネントをDOM反映

RTLなし
import { render } from "react-dom";
import App from "./App";

test(("render App") => {
  const container = document.createElement("div");
  render(<App />, container);
});

Reactコンポーネントをレンダリング対象となるdiv要素をdocument.createElement("div")で生成し、react-domのrender関数でReactコンポーネントをDOMに反映している。

React Testing Libraryあり
import { render } from "@testing-library/react";
import App from "./App";

test("render App", async () => {
  render(<App />);

👉RTLだと@testing-library/reactのrender関数で以下のことを網羅する。
・土台のdiv要素をDOM上に生成。
・引数のReactコンポーネントをDOM(土台のdiv要素)に反映。

02. DOM内の要素取得

RTLなし
import { render } from "react-dom";
import App from "./App";

test(("find the button to count up") => {
  const container = document.createElement("div");
  render(<App />, container);
  // find the button
  const button = document.querySelector("[data-testid=toggle]");
  // ...
});

DOM要素参照でお馴染みのdocument.querySelector関数を使って参照する。

RTLあり
import { render, screen } from "@testing-library/react";
import App from "./App";

test("find the button to count up", async () => {
  render(<App />);
  // find the button
  const button = screen.getByTestId("countupBtn");
  // ...

👉RTLだとscreenオブジェクトの関数(getByXxx/findByXxx/queryByXxx)でらくらく参照。※内部でdocument.querySelector関数などを使っているのだろう。

03. イベント発火

RTLなし
import { render } from "react-dom";
import { act } from "react-dom/test-utils";
import App from "./App";

test(("find the button to count up") => {
  const container = document.createElement("div");
  render(<App />, container);
  // find the button
  const button = document.querySelector("[data-testid=toggle]");
  // click the button
  act(() => {
    button.dispatchEvent(new Event("click"));
  });
});

DOM APIのEventTarget.dispatchEvent関数を使って発火する。今回の例だとClickイベントを発火させている。

RTLあり
import { render, fireEvent, waitFor, screen } from "@testing-library/react";
import App from "./App";

test("find the button to count up", async () => {
  render(<App />);
  // find the button
  const button = screen.getByTestId("countupBtn");
  // click the button
  await waitFor(() => {
    fireEvent.click(button);
  });

👉RTLだとfireEventオブジェクトの関数(click/blur/drag/etc)でらくらくイベント発火。

04. Custom Matcher(@testing-library/jest-dom)

RTLが提供する@testing-library/jest-domを下記のようにimportすることで、DOMから要素を取得するための便利なメソッドを利用することができる。

import { render, fireEvent, waitFor, screen } from "@testing-library/react";
import "@testing-library/jest-dom"; // ★ここ★ //
import App from "./App";

test("should diplay 1 after clicking the button once", async () => {
  render(<App />);
  // get by data-testid
  const button = screen.getByTestId("countupBtn");

  await waitFor(() => {
    fireEvent.click(button);
  });

  // ★ここ★ //
  expect(await screen.findByText("COUNT: 1")).toBeInTheDocument();
});

※リンク先↓に利用可能なCustom Matcherがあるので参考に。

中でも良く使うものをメモしておく。

関数名 役割
toBeInTheDocument() 対象要素がdocument内にあること
toBeVisble() 対象要素がVisbleかどうか
toHavaAttribute(attr: string, value?: any) 対象要素が引数の属性を持つか

RTL関連メモ

👉act関数とRTLのwaitFor関数の話

✅act関数を使うタイミング、理由
✅RTLが提供するfireEventやrender関数は内部でact関数を利用してる。
RTLのwaitFor関数を使えばact関数を使わないで済むこと

参考:React のテストを書いてたら act で囲んでよーって言われたとき

act関数とは

waitFor関数とは

👉getByXxx / findByXxx / queryByXxx の使い分け

こちらの記事が参考になったので引用させていただく。

get 〇〇、query 〇〇、find 〇〇では、該当する要素がある時とない時で Error を返すかどうかが違う。

👉fireEvent VS @testing-library/user-event

RTLの公式ドキュメント(Firing Events)では、下記のように@testing-library/user-eventを使ったほうがいいでと書いてある。

Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.

理由について分かりやすく説明してくれている人が既にいるので、分かりやすかったものを引用させていただく。React Testing Library では fireEvent よりも userEvent を使ったほうがいいらしい

RTL vs Enzyme

これについても沢山の人が議論してくれているので、分かりやすかったものを引用させていただく。

✅Enzyme -> コンポーネント内部のstateや子コンポーネントをMockすることができる。
✅RTL -> Enzymeのようにコンポーネント内部はいじれない。Userが実際に操作するのと同じようなテストを書く。

From Difference between enzyme, ReactTestUtils and react-testing-library

Enzyme allows you to access the internal workings of your components. You can read and set the state, and you can mock children to make tests run faster.

On the other hand, react-testing-library doesn't give you any access to the implementation details. It renders the components and provides utility methods to interact with them. The idea is that you should communicate with your application in the same way a user would. So rather than set the state of a component you reproduce the actions a user would do to reach that state.

From EnzymeよりReact Testing Libraryでしょ〜

EnzymeよりReact Testing Libraryを使うべき理由は大きくまとめて以下2つです。
・アプリケーションの効率的なテスト手法であるTesting Trophyに則ったIntegrationテストに特化したテストツールであること
実装の詳細に依存しないテストを書くことで開発者のストレス軽減につながること
そしてEnzymeはReact17、18を公式にはサポートしていないので、そちらを踏まえても現環境ではReact Testing Libraryを使うべきと言えると思います。

Discussion