💨

フロントエンドテスト 入門 React Testing Library編

2023/05/30に公開

今回は、フロントエンドテストの方法について解説していきます。

フロントエンドでは様々なテストがありますが、今回はReact Testing Libraryの使い方に絞って解説していきます。

以前にJest編の記事も作ったので、そちらの続きとして進めていこうと思います。

Hello World

まずは、Reactのテストがどのように実行されるかを簡単に解説していきます。

create-react-appでプロジェクトを作ったと仮定して進めていきます。

新しくコンポーネント用のファイルを作成します。

const Hello = () => {
  return <h1>Hello World</h1>;
};

export default Hello;

次のこのコンポーネントが、正しく表示されているかをテストします。

import { render, screen } from "@testing-library/react";
import Hello from "./Hello";

test("h1が存在するか", () => {
  render(<Hello />);

  const h1EL = screen.getByText("Hello World");
  expect(h1EL).toBeInTheDocument();
});

まず、renderメソッドでコンポーネントを擬似的にレンダリングすることができます。

そして、レンダリングされた画面をscreenから取得することができます。

最後に、取得した要素が想定通りにあるかをテストしています。

ちなみにTypescriptを使う場合は、拡張子をtsではなくtsxにしないとエラーになるみたいなので注意です。

要素の取得

先ほどはgetByTextを使って要素を取得しましたが、他にも取得するための関数はいくつかあります。

まず大前提として、次の3種類に分けることができます。

  • getBy
  • queryBy
  • findBy

getByは要素を探しにいき、見つからなければエラーが返されます。

次に、queryByではnullが返ってきます。

そして、最後のfindByは非同期関数に使用することができます。

これについては後ほど解説します。

ちなみに、~ByAllとすることで、対象の要素を全て取得することができます。

そして、要素取得用の主な関数をまとめると、次のようになります。

  • getByRole
  • getByLableText
  • getByPlaceholderText
  • getByText
  • getByDisplayValue
  • getByAltText
  • getByTitle
  • getByTestId

一応、上から順に優先的に使うべきとされています。

詳しい説明はこちらの記事を参考にしてください。

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

マッチャー

次に、DOMに関するマッチャーについて解説していきます。

よく使うものを表にまとめると、次のようになります。

マッチャー 説明
toBeDisabled disabledかチェック
toBeEnabled disabledじゃないかチェック
toBeEmptyDOMElement DOMの中身が空かチェック
toBeInTheDocument DOMの中身に対象があるかチェック
toBeInvalid input要素などがinvalidかチェック
toBeRequired input要素などrequiresかチェック
toBeValid input要素などがinvalidではないかチェック
toBeVisible 要素が目に見える状態かチェック
toContainElement 要素を子要素として持っているかチェック
toContainHTML 要素を子要素として持っているかチェック
toHaveClass 対象のクラスを持っているかチェック
toHaveFocus focus状態かチェック
toHaveFormValues formの値をチェック
toHaveStyle 対象のstyleをチェック
toHaveTextContent 中の文字要素をチェック
toHaveValue inputなどのvalueをチェック
toBeChecked チェック状態かチェック

User Event

次は、ユーザーのインタラクションな操作に対するテストの方法を解説していきます。

まず、react-testing-libraryにもfireEventというイベントの発火を再現できる関数がありますが、これはあまり使わない方が良いです。

なぜなら、ユーザーの操作がさらに忠実に再現されているuserEventという関数があるからです。

なので、ユーザーの操作をテストしたい時は、このuserEventを使用するようにしましょう。

では早速、ユーザーが入力した値を画面に表示するコンポーネントを作成します。


import React from "react";

const Input = () => {
  const [value, setValue] = React.useState<string>("");
  return (
    <div>
      <h1>入力された文字: {value}</h1>
      <input type="text" onChange={(e) => setValue(e.target.value)} />
    </div>
  );
};

export default Input;

次に、この入力された値が問題なく画面に表示できているかをテストしていきます。

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

test("input", () => {
  render(<Input />);

  // 要素の取得
  const h1El = screen.getByRole("heading");
  const inputEl: HTMLInputElement = screen.getByRole("textbox");

  // 初期状態のテスト
  expect(h1El.textContent).toBe("入力された文字: ");
  expect(inputEl.value).toBe("");
  
  // 値の入力
  userEvent.type(inputEl, "test");

  // 入力後のテスト
  expect(h1El.textContent).toBe("入力された文字: test");
  expect(inputEl.value).toBe("test");
});

このように、input要素やボタン要素などのインタラクション系のテストも、testing-libraryを使用して実行することができます。

非同期処理

次に、非同期処理のテストについて見ていきます。

まずは、非同期処理を使用したコンポーネントを作成します。

import React, { useEffect } from "react";
import "./App.css";

function App() {
  const [value, setValue] = React.useState<any>({});
  useEffect(() => {
    fetch("https://qiita.com/api/v2/items")
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setValue(data[0]);
      });
  }, []);
  return <p>{value.title ? `Title is ${value.title}` : null}</p>;
}

export default App;

そして、このコードのテストファイルを作成します。

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

test("render App", () => {
  render(<App />);
  const linkElement = screen.queryByText(/Title is/i);
  expect(linkElement).toBeNull();
});

このように、普通にrenderしても非同期関数がある場合は、fetch前の画面をレンダリングすることになります。

なので、Title is という文字列は見つからずに、nullが返ってきます。

では、データ取得後のコンポーネントをテストしたい場合はどうしたら良いかと言うと、findByメソッドを使用すればOKです。

先ほども少し説明しましたが、findByメソッドはデータ取得まで待機させることができます。

なので、次のようにテストコードを書けばOKです。

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

test("render App", async () => {
  render(<App />);
  const linkElement = await screen.findByText(/Title is/i);
  expect(linkElement).toBeInTheDocument();
});

find系のメソッドは非同期関数なので、async/awaitを忘れないようにしましょう。

カスタムフック

React testing-libraryには、カスタムフック専用のテストも存在します。

それは、actrenderHookです。

その関数を試すために、まず簡単なカスタムフックを定義します。

import { useState } from "react";

export const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((prev) => prev + 1);
  const decrement = () => setCount((prev) => prev - 1);
  return { count, increment, decrement };
};

今回は、簡単なカウンターのhooksを用意しました。

そして、これのテストコードは次のようになります。

import { renderHook } from "@testing-library/react";
import { useCounter } from "./useCounter";
import { act } from "react-dom/test-utils";

describe("useCounter", () => {
  it("should be increment", () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it("should be decrement", () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);

    act(() => {
      result.current.decrement();
    });
    expect(result.current.count).toBe(-1);
  });
});

このように、まずrenderHookでhooksの初期化ができます。

次にactを使用することで、hooksに定義された関数を実行することができます。

なので、先ほどのようにテストを書くことで、hooksのテストもすることができるのです。

まとめ

今回は、Reactのtesting-libraryを使用したテストの方法を解説してきました。

今やほとんどの現場で使用されている技術なので、この機会に身につけましょう。

宣伝

0からエンジニアになるためのノウハウをブログで発信しています。
https://hinoshin-blog.com/

また、YouTubeでの動画解説も始めました。
https://www.youtube.com/channel/UCqaBUPxazAcXaGSNbky1y4g

インスタの発信も細々とやっています。
https://www.instagram.com/hinoshin_enginner/

興味がある方は、ぜひリンクをクリックして確認してみてください!

Discussion