はじめに
この記事では、React Testing Library (RTL) のページ上の要素を取得する API の使い分け方をサンプルつきで記載していきます。
前提
サンプルを実装した開発環境は以下の通りです。
開発環境は以下の通りです。
- Windows11
- VSCode
- TypeScript 4.9.5
- React 18.2.0
- Vite 4.1.0
- Vitest 0.28.5
- @testing-library/react 14.0.0
また、事前に以下の記事に記載の手順に沿って、Vitest で React Testing Library が利用できるように設定しておきます。
要素を取得する API の分類
ページ上の要素を取得する API は、以下の3点で分類できます。
- 取得する要素の数:単一(
...By...
)、複数(...AllBy...
) - クエリの種類:
getBy (getAllBy)...
、queryBy (queryAllBy)...
、findBy (findAllBy)...
- クエリの方法:
...ByRole
、...ByLabelText
、...ByPlaceholderText
、...ByText
、...ByDisplayVaalue
、...ByAltText
、...ByTitle
、...ByTestId
クエリの種類
getBy (getAllBy)
、queryBy (queryAllBy)
、findBy (findAllBy)
は、以下のように要素が見つからないときの挙動と非同期処理の扱いによって分類できます。
クエリの種類 | 要素が見つからない | 非同期 |
---|---|---|
getBy (getAllBy) | エラー | × |
queryBy (queryAllBy) | null or [] | × |
findBy (findAllBy) | エラー | 〇 |
getBy (getAllBy)
のユースケース
特に理由がなければ(queryBy (queryAllBy)
や findBy (findAllBy)
を使う必要がなければ)、getBy (getAllBy)
を利用します。
例えば、単に以下のようなコンポーネント上のテキストを取得するときは、getByText
を利用します。
export const Home = () => {
return <div>You are home</div>;
};
import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { describe } from "vitest";
import { Home } from "../../../pages/Home";
describe("Home component", () => {
test("elements", async () => {
render(<Home />, { wrapper: BrowserRouter });
expect(screen.getByText(/you are home/i)).toBeInTheDocument();
});
});
queryBy (queryAllBy)
のユースケース
queryBy
は、要素が存在しない場合、null
を返す(queryAllBy
の場合、[]
)ため、存在しない要素に対して利用されます。
例えば、以下のように props
の値によって、テキストの表示・非表示が変わる要素があるとします。
export const Home = ({ authorized }: { authorized: boolean }) => {
return (
<>
{authorized && <div>You are authorized</div>}
</>
);
};
この要素のテキストが表示されている場合のテストは、getByText
を利用すれば、問題なくテストできます。
describe("Home component", () => {
test("when authorized", async () => {
render(<Home authorized={true} />, { wrapper: BrowserRouter });
expect(screen.getByText(/you are authorized/i)).toBeInTheDocument();
});
});
一方、テキストが非表示の場合のテストは、getByText
を利用すると、エラーになってしまいます。
test("when not authorized", async () => {
render(<Home authorized={false} />, { wrapper: BrowserRouter });
expect(screen.getByText(/you are authorized/i)).not.toBeInTheDocument();
});
そのため、getByText
の代わりに queryBy
を利用します。
test("when not authorized", async () => {
render(<Home authorized={false} />, { wrapper: BrowserRouter });
expect(screen.queryByText(/you are authorized/i)).not.toBeInTheDocument();
});
エラーが発生することなく、非表示であることのテストができました。
findBy (findAllBy)
のユースケース
findBy (findAllBy)
は、要素を非同期に取得することができるため、state の値が変わったことによるUIの変化や非同期APIのレスポンスが返ってきた後のUI表示などに利用できます。
例えば、以下のようにボタンをクリックすることでボタン上のテキストが変わる要素があるとします。
import { useState } from "react";
export const Home = () => {
const [buttonText, setButtonText] = useState("button");
return <button onClick={() => setButtonText("clicked")}>{buttonText}</button>;
};
ボタンクリック後にテキスト表示が変化することをテストする際、getByText
を利用するとエラーになってしまいます。
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { describe } from "vitest";
import { Home } from "../../../pages/Home";
describe("Home component", () => {
test("when authorized", async () => {
render(<Home />, { wrapper: BrowserRouter });
userEvent.click(screen.getByRole("button"));
expect(screen.getByText("clicked")).toBeInTheDocument();
});
});
一方、findByText
を await
と一緒に利用し、非同期に要素を取得すれば、エラーなく、テストをすることができます。
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { describe } from "vitest";
import { Home } from "../../../pages/Home";
describe("Home component", () => {
test("test", async () => {
render(<Home />, { wrapper: BrowserRouter });
userEvent.click(screen.getByRole("button"));
expect(await screen.findByText("clicked")).toBeInTheDocument();
});
});
以下の書き方であれば、 getByText
を利用してもエラーなく、テストをすることができます。
方法1. クリック処理に await
をつける
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { describe } from "vitest";
import { Home } from "../../../pages/Home";
describe("Home component", () => {
test("test", async () => {
render(<Home />, { wrapper: BrowserRouter });
await userEvent.click(screen.getByRole("button"));
expect(screen.getByText("clicked")).toBeInTheDocument();
});
});
方法2. waitFor
を利用する
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { describe } from "vitest";
import { Home } from "../../../pages/Home";
describe("Home component", () => {
test("test", async () => {
render(<Home />, { wrapper: BrowserRouter });
userEvent.click(screen.getByRole("button"));
await waitFor(() => {
expect(screen.getByText("clicked")).toBeInTheDocument();
});
});
});
クエリの方法
RTL は以下の思想をもとに作られています。
The more your tests resemble the way your software is used, the more confidence they can give you.
こちらの思想にもとづき、クエリの方法(By...
)は優先度は以下の順番で利用することが推奨されています。
- Queries Accessible to Everyone
getByRole
getByLabelText
getByPlaceholderText
getByText
getByDisplayValue
- Semantic Queries
getByAltText
getByTitle
- Test IDs
getByTestId