最近、韓国ドラマと個人開発に勤しんでいる大角です。
Webサービスを提供する上でテストを自動化するケースも多いと思いますが、今回はReact Testing Libraryを使った基本的なテストの書き方についてご紹介したいと思います。これからReactでユニットテストを始めたい方などのご参考になれば、幸いです。
テストの内容については3回の記事に分け、今回の記事では最初のReactコンポーネントのテストを取り上げます。
- React Componentのテスト(今回の記事)
- カスタムフックのテスト
- Recoilのテスト
なお、執筆時点での関連するパッケージのバージョンは以下になっています。
- create-react-app: 5.0.1
- react: 18.1.0
- @testing-library/jest-dom: 5.16.4
- @testing-library/react: 13.2.0
- @testing-library/user-event: 13.5.0
また、以下の点については取り上げていませんので、予めご了承ください。
- テストケースの設計
- Jestの書き方、基礎知識
React Testing Library
React Testing Libraryは、Kent.C.dodds氏が作ったReact向けのテストライブラリです。
よくEnzymeと比較されるようですが、React Testing Libraryはユーザーの操作と同じような感覚でテストができる点が特徴だと言われています。create-react-appでアプリを作成した際にもReact Testing Libraryが組み込まれていますよね。
たとえば、Appコンポーネント内に”Search:”というテキストが存在するかどうかをテストする場合、以下のようなコードになります。
import { render, screen } from '@testing-library/react'; import App from './App'; describe('App', () => { test('renders App component', () => { render(<App />); expect(screen.getByText('Search:')).toBeInTheDocument(); }); });
よく利用するもの
React Testing Libraryを使ってテストを書く際に利用する基本的な関数をご紹介します。
コンポーネントの描画
render
関数を利用して、Reactコンポーネントを描画することができます。
render(<App />);
もし、props
を変更することによって表示結果が変わることを検証したい場合はrerender
関数を利用することもできます。
import {render} from '@testing-library/react'; const {rerender} = render(<NumberDisplay number={1} />); expect(screen.getByTestId('number-display')).toHaveTextContent('1') rerender(<NumberDisplay number={2} />); expect(screen.getByTestId('number-display')).toHaveTextContent('2');
debug
関数を利用することで描画結果をコンソールに出力することもできます。
import {render} from '@testing-library/react'; const HelloWorld = () => <h1>Hello World</h1>; const {debug} = render(<HelloWorld />); debug(); // <div> // <h1>Hello World</h1> // </div>
https://testing-library.com/docs/react-testing-library/api/#debug
要素の取得
コンポーネントの描画結果から要素を取得できます。
screen.getByText('Search:');
screen
オブジェクトには以下のような要素取得関数があります。
getByText
: 引数に渡されたテキストにマッチする要素を返します。マッチする要素が無かったり、要素が複数存在する場合はエラーを返します。queryByText
: 引数に渡されたテキストにマッチする要素を返します。マッチする要素が無い場合はnullを返します。これは、存在しない要素を検出するのに便利です。複数の要素が存在する場合はエラーを返します。findByText
: 非同期で引数に渡されたテキストにマッチする要素を返します。要素が見つからないか、タイムアウトした場合はrejectされます。
要素が常に存在する場合はgetByText
で十分ですが、要素が存在しないケースがある場合はqueryByText
を利用したり、要素が非同期で表示される場合はfindByText
を利用するなどで使い分けます。
また、テキストで要素を取得する以外にも、getByRole
や getByLabelText
など別の情報をもとに要素を取得する関数もあります。
// button要素を取得する screen.getByRole("button"); // aria-labelをもとに要素を取得する screen.getByLabelText("submit");
詳細は以下のドキュメントを参照してください。
https://testing-library.com/docs/react-testing-library/cheatsheet/#queries
アクションの実行
fireEvent
オブジェクトを利用して、ユーザーのアクションを実行することができます。
const mockFunc = jest.fn(); fireEvent.click(screen.getByRole('button')); expect(mockFunc).toHaveBeenCalled();
fireEvent
よりもユーザーの挙動を忠実に模倣するためのuserEvent
もあります。たとえば、userEvent
を利用してキーをタイプした場合、keyDown
などが文字の数だけ実行されます。
await userEvent.type(screen.getByRole('textbox'), 'JavaScript');
アサーション関数
Jestに元々アサーション関数は用意されていますが、React Testing Libraryは独自のアサーション関数を提供しています。たとえば、特定の要素がドキュメント内に存在するかどうかをチェックする際には toBeInTheDocument
のようなアサーション関数を使うことができます。
render(<App />); expect(screen.getByText('Search:')).toBeInTheDocument();
他にもイベントの実行回数をチェックするためのtoHaveBeenCalledTimes
など様々なアサーションが用意されています。
const onChange = jest.fn(); render(<Select value="item1" onChange={onChange} />); fireEvent.change(screen.getByRole('select'), { target: { value: 'item2' } }); expect(onChange).toHaveBeenCalledTimes(1);
詳細は以下のドキュメントを参照してください。
https://github.com/testing-library/jest-dom#custom-matchers
サンプルコード
最後にこれまでご紹介した関数などを利用して、カウンターのようなUIコンポーネントをテストするコードを書いてみると、以下のようになります。
// Counter/index.tsx import { FC } from "react"; type Props = { value: number; onChange: (value: number) => void; }; export const Counter: FC<Props> = ({ value, onChange }) => ( <div> <button type="button" onClick={() => onChange(value - 1)} aria-label="decrement" > - </button> <output>{value}</output> <button type="button" onClick={() => onChange(value + 1)} aria-label="increment" > + </button> </div> );
このカウンターに対して、以下の3つのテストケースを書いてみます。
- props valueに渡した数値が表示されていること
- incrementした場合に加算されていること
- decrementした場合に減算されていること
// Counter/index.test.tsx import { fireEvent, render, screen } from "@testing-library/react"; import { Counter } from "./index"; describe("Counter", () => { test("show value", () => { const onChange = jest.fn(); render(<Counter value={0} onChange={onChange} />); expect(screen.getByText("0")).toBeInTheDocument(); }); test("decrement value", () => { const onChange = jest.fn(); render(<Counter value={0} onChange={onChange} />); fireEvent.click(screen.getByLabelText("decrement")); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toBeCalledWith(-1); }); test("increment value", () => { const onChange = jest.fn(); render(<Counter value={0} onChange={onChange} />); fireEvent.click(screen.getByLabelText("increment")); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toBeCalledWith(1); }); });
おわりに
いかがでしたでしょうか。この記事では基本的な部分をご紹介しましたが、さらに詳しく知りたい方は公式のチュートリアルが分かりやすくて、オススメです。
次回はカスタムフックのテストについて書いてみたいと思います!