こんにちは、かつコーチです。
前回は、テストフレームワークJestの基本文法と環境構築を解説しました。
sumのような単純な関数のテストはできるようになりましたが、Reactのコンポーネントは「画面に何が表示されるか」「ボタンを押したらどう変化するか」といった、DOMを絡めた検証が必要になります。
そこで登場するのが、今回紹介するReact Testing Libraryです。
Jestと組み合わせて使うことで、Reactコンポーネントらしいテストが書けるようになります。
React Testing Libraryとは?
コンポーネントを「ユーザー視点」でテストするライブラリ
React Testing Library(RTL)は、Reactコンポーネントをレンダリングし、その結果のDOMに対して検証を行うためのライブラリです。
最大の特徴は、「実装の詳細」ではなく「ユーザーが実際に見て・触る内容」を基準にテストを書くという設計思想にあります。
たとえば「このコンポーネントのstateの値が◯◯になっている」ではなく、「画面に『送信完了しました』という文字が表示されている」というように、ユーザーの目線でテストを組み立てます。
この考え方のおかげで、内部実装(stateの持ち方など)をリファクタリングしても、見た目の挙動が変わらなければテストが壊れにくいというメリットがあります。
JestとRTLの役割分担
JestとRTLは、それぞれ役割が異なります。
- Jest:テストを実行する土台(
describe・it・expectなどの仕組み) - React Testing Library:Reactコンポーネントをレンダリングし、DOMを操作・検証するための道具
「テストの実行環境がJest、コンポーネントを扱うための道具がRTL」というイメージを持っておくと整理しやすいです。
環境構築
必要なパッケージのインストール
前回のJest環境に、RTL関連のパッケージを追加します。
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
それぞれ役割が異なります。
@testing-library/react:コンポーネントをレンダリングするための本体@testing-library/jest-dom:toBeInTheDocument()など、DOM検証用のマッチャーを追加する@testing-library/user-event:クリックや入力といったユーザー操作をシミュレートする
Jestの設定ファイルを更新する
@testing-library/jest-domのマッチャーを有効にするため、セットアップファイルを用意します。
// jest.setup.ts
import "@testing-library/jest-dom";
// jest.config.ts
import type { Config } from "jest";
const config: Config = {
preset: "ts-jest",
testEnvironment: "jsdom",
setupFilesAfterEach: [],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
testMatch: ["**/*.test.ts", "**/*.test.tsx"],
};
export default config;
setupFilesAfterEnvに設定ファイルを指定することで、すべてのテストファイルでtoBeInTheDocument()のようなマッチャーが使えるようになります。
基本の使い方
renderでコンポーネントを描画する
RTLの基本的な流れは、「コンポーネントを描画する」→「要素を取得する」→「検証する」の3ステップです。
// Greeting.tsx
type GreetingProps = {
name: string;
};
export function Greeting({ name }: GreetingProps) {
return <p>こんにちは、{name}さん</p>;
}
// Greeting.test.tsx
import { render, screen } from "@testing-library/react";
import { Greeting } from "./Greeting";
test("名前を含んだ挨拶メッセージが表示される", () => {
render(<Greeting name="かつコーチ" />);
const message = screen.getByText("こんにちは、かつコーチさん");
expect(message).toBeInTheDocument();
});
renderでコンポーネントを描画し、screen.getByTextで「こんにちは、かつコーチさん」というテキストを持つ要素を探し出します。
toBeInTheDocument()は、@testing-library/jest-domが提供するマッチャーで、「その要素がDOM上に存在するか」を検証します。
要素を探す優先順位
RTLには要素を探すための関数が複数用意されていますが、公式ではアクセシビリティに基づいた探し方が推奨されています。
優先度が高い順に並べると、次のようなイメージです。
getByRole(ボタン・見出しなど、要素の役割で探す)getByLabelText(フォームのラベルで探す)getByText(表示されているテキストで探す)getByTestId(data-testid属性を使って探す。最終手段)
test("ボタンをroleで取得する", () => {
render(<button>送信する</button>);
const button = screen.getByRole("button", { name: "送信する" });
expect(button).toBeInTheDocument();
});
getByTestIdは便利ですが、実際のユーザーには見えない属性に依存する探し方なので、できるだけgetByRoleやgetByTextで書けないか先に検討する、というのがRTL流の考え方です。
つまずきやすいポイント:getByとqueryByの使い分け
要素が「存在しないこと」を検証しようとしてエラーになる
私が最初にRTLでつまずいたのが、「要素が表示されていないこと」を検証しようとしたときのエラーです。
❌ Before:getBy系で「存在しないこと」を検証しようとする
test("エラーメッセージは最初は表示されていない", () => {
render(<Form />);
const errorMessage = screen.getByText("入力内容に誤りがあります");
expect(errorMessage).not.toBeInTheDocument();
// 実行時エラー:そもそもgetByTextの時点で「見つからない」という例外が発生する
});
getByTextは、対象の要素が見つからない場合、その場で例外を投げる仕様になっています。
「存在しないことを確認したいのに、存在しないことを確認する前にエラーで落ちる」という状態になり、最初は何が起きているのか理解できずに戸惑いました。
✅ After:存在チェックにはqueryBy系を使う
test("エラーメッセージは最初は表示されていない", () => {
render(<Form />);
const errorMessage = screen.queryByText("入力内容に誤りがあります");
expect(errorMessage).not.toBeInTheDocument();
// 成功:見つからない場合はnullが返るだけなのでエラーにならない
});
queryByTextは、要素が見つからない場合に例外を投げず、nullを返してくれます。
「要素が存在することを確認したいときはgetBy」「存在しないことを確認したいときはqueryBy」という使い分けを覚えてからは、このエラーに悩まされることはなくなりました。
まとめ
この記事のポイント
- React Testing Libraryは「ユーザー視点」でコンポーネントをテストするためのライブラリ
render→要素の取得→expectでの検証という流れが基本- 要素の取得は
getByRoleなど、アクセシビリティに基づいた方法を優先する - 存在確認は
getBy、非存在確認はqueryByを使い分ける
次に読むべき記事
RTLの基本的な使い方が分かったところで、次は実際にユーザー操作(クリックや入力)を伴うコンポーネントのユニットテストを書いていきましょう。
→ 次の記事:コンポーネントのユニットテストを書く