はじめに
ReactTestingLibrary(RTL)を利用する際に、要素を取得する方法をまとめました。本記事では単純にレンダリングのテストを行っているだけに留まりますが、RTLを使ってテストを始める取っ掛かりとして参考になればと思います。
対象読者
Reactで開発してて、これからテストを導入したい人
サンプルリポジトリはこちらです(参考程度に)
① テストファイルの中でコンポーネントをレンダリングする。
RTLは、Render結果であるDOM構造をテストするので実際のユーザー行動に近いテストをし易いのが特徴です。
コンポーネントのユニットテストをするには、まずはテストファイル内にコンポーネントをimportしてレンダリングしてあげる必要があります。そのためには、render
関数を使用します。
render
任意のJSXを受け取ってレンダリングする関数です。
render関数実行後、Reactコンポーネントにアクセスできるようになります。
これがコンポーネントのユニットテストを書く際の初めの一歩です。
describe("RenderingTest", () => {
test("正しく表示されてること", () => {
render(<RenderTest />);
});
});
debug
render関数でReactコンポーネントにアクセスできるようにしたら、debug関数によってコンポーネントのHTML構造を確認することができます。実際に確認してみましょう。
screen.debug()
ショートハンドで書くこともできます。
describe("RenderingTest", () => {
test("正しく表示されてること", () => {
const { debug } = render(<RenderTest />);
debug(); // コンソールにレンダリング結果を出力
});
});
debug関数を実行すると以下のような結果が得られます。
console.log
<body>
<div>
<div>
<h2>
RenderTestSample
</h2>
<div>
<img
alt="ReactLogo"
src="/logo192.png"
/>
</div>
<div>
これはレンダリングテストのサンプルです。
<span>
forStudy
</span>
</div>
<div>
<label
for="count"
>
Count:
</label>
<input
id="count"
placeholder="Enter"
type="text"
value="defaultValue"
/>
</div>
<button
name="increment"
type="button"
>
Increment
</button>
<button
name="decrement"
type="button"
>
Decrement
</button>
<button
name="reset"
type="button"
>
Reset
</button>
</div>
</div>
</body>
単一または複数の要素を抽出してデバッグすることもできます。
// 単一要素
screen.debug(screen.getByText("これはレンダリングテストのサンプルです。"))
// 複数要素
screen.debug(screen.getAllByText("これはレンダリングテストのサンプルです。"))
これでRTLから見えるHTML構造が確認できたので、これを元にテストを書いていく準備ができました。
次に、検証したい要素を取得する必要があります。
② 検証対象の要素を取得する
レンダリングした後は、RTLに用意された検索関数を使って、要素を取得していきます。
要素の選択をする際は、screen.[query]
で取得できます。
screen.getByRole("heading");
クエリのタイプは以下の通りです。これらの違いは、要素が見つからない場合にエラーをスローするか、Promiseを再試行するかで、状況に応じて適切なクエリを使って要素を取得する必要があります。
単一要素を取得するクエリ
公式ドキュメントにもまとめられてますが、一つずつ見ていきましょう。
getBy
クエリに一致するノードを返します。
一致する要素がない場合、または複数の一致が検出された場合にもエラーを投げます。
要素を取得する際の最も基本的なクエリーになるので、まずはgetByを使うことを検討します。
queryBy
クエリに一致するノードを返し、一致する要素がない場合、null
を返します。
複数の一致が検出された場合はエラーを投げます。
要素がないことをテストしたい時に役に立ちます。
findBy
指定されたクエリに一致する要素が見つかったときに、resolveを返します。
デフォルトのタイムアウト後に、要素が見つからない場合、または複数の要素が見つかった場合、rejectを返します。
ボタンを押下後、DOMの変更を待って要素を取得したい等の場合に利用します。
※単一要素を取得するクエリの注意点
これらはクエリーに一致する要素が複数見つかった場合はエラーを投げるので、その場合は後述する「複数要素を取得する際のクエリ」を使う必要があります。
複数要素を取得するクエリ
getAllBy
クエリに一致するすべてのノードの配列を返します。
一致する要素がない場合はエラーを投げます。
queryAllBy
クエリに一致するすべてのノードの配列を返します。
一致する要素がない場合は、空配列を返します。
findAllBy
指定されたクエリに一致する要素が見つかった場合に、resolveを返します。
デフォルトのタイムアウト後に要素が見つからなかった場合、rejectを返します。
##実際に要素を取得してみる
上記のクエリーを使って実際に要素を取得してみます。
以下に単純なコンポーネントがあります。
import React from "react";
export const Render: React.FC = () => {
return (
<div>
<h2>RenderTestSample</h2>
<div>
<img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="ReactLogo" />
</div>
<div>
これはレンダリングテストのサンプルです。<span>forStudy</span>
</div>
<div>
<label htmlFor="count">Count: </label>
<input
id="count"
type="text"
placeholder="Enter"
value="defaultValue"
/>
</div>
<button type="button" name="increment">
Increment
</button>
<button type="button" name="decrement">
Decrement
</button>
<button type="button" name="reset">
Reset
</button>
</div>
);
};
getByRole
以下のリンクで取得できる要素を確認することができます。
expect(screen.getByRole("heading")).toBeTruthy();
上記でheadingタグを抽出することはできますが、例えば、h1
とh2
がコンポーネントの中に存在する場合はどうするでしょうか?
この場合、第二引数に以下のoptionsを渡すことで、特定要素を抽出することができます。
以下に例を示します。
options?: {
exact?: boolean = true,
hidden?: boolean = false,
name?: TextMatch,
normalizer?: NormalizerFn,
selected?: boolean,
checked?: boolean,
pressed?: boolean,
current?: boolean | string,
expanded?: boolean,
queryFallbacks?: boolean,
level?: number,
}
// heading要素を「level」でフィルタリングする
expect(screen.getByRole("heading", { level: 1 })).toBeTruthy(); // h1
expect(screen.getByRole("heading", { level: 2 })).toBeTruthy(); // h2
// button要素を「name」でフィルタリングする
expect(screen.getByRole("button", { name: /increment/i })).toBeTruthy(); // name属性が「increment」
expect(screen.getByRole("button", { name: /decrement/i })).toBeTruthy(); // name属性が「decrement」
getByLabelText
フォームフィールドを抽出する際に適したメソッドです。
例えばコンポーネントの中に以下の要素があった場合
<label htmlFor="count">Count</label>
<input id="count" type="text" placeholder="Enter" />
getByLabelText
を使用すると以下の結果が得られます。
debug(screen.getByLabelText("Count"));
getByPlaceholderText
フォームフィールドを取得したいけど、label
が定義されていないというケースもあると思います。
その際はgetByPlaceholderText
を使って取得しましょう。これでも、consoleには上記と同じ結果が出力されます。
debug(screen.getByPlaceholderText("Enter"));
getByText
divやspan、pタグの要素を見つける際に役立ちます。
コンポーネントの中に以下の要素があった場合、、、
<div>
これはレンダリングテストのサンプルです。<span>forStudy</span>
</div>
getByText
を使用すると以下の結果が得られます。
親要素のテキストで抽出すると子要素も取得されるので、子要素のみ取得したい時は、対象のテキストを引数に渡しましょう。
debug(screen.getByText("これはレンダリングテストのサンプルです。"));
debug(screen.getByText("forStudy"));
getByDisplayValue
フォーム要素に入力されてる値から要素を取得するメソッドです。
値の入力をテストする際に役立ちます。
<input id="count" type="text" placeholder="Enter" value="defaultValue" />
debug(screen.getByDisplayValue("defaultValue"));
getByAltText
主にimgタグ、その他inputタグ、areaタグを取得する際に使用します。
<div>
<img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="ReactLogo" />
</div>
debug(screen.getByAltText("ReactLogo"));
③ 取得した要素を検証する
レンダリング結果をテストしているだけなので、非常にシンプルなテストですが、以下がテストコードのサンプルです。
import { render, screen } from "@testing-library/react";
import { Render } from "../../pages/TestSample/components/Render";
describe("RenderingTest", () => {
test("正しく表示されてること", () => {
const { debug } = render(<Render />);
debug(); // デバッグ
expect(screen.getByRole("heading")).toBeTruthy();
expect(screen.getByRole("textbox")).toBeTruthy();
expect(screen.getByRole("button", { name: /increment/i })).toBeTruthy();
expect(screen.getAllByRole("button")[0]).toBeTruthy();
expect(screen.getAllByRole("button")[1]).toBeTruthy();
expect(screen.getByLabelText("Count:")).toBeTruthy();
expect(screen.getByPlaceholderText("Enter")).toBeTruthy();
expect(
screen.getByText("これはレンダリングテストのサンプルです。")
).toBeTruthy();
expect(screen.getByText("forStudy")).toBeTruthy();
expect(screen.getByDisplayValue("defaultValue")).toBeTruthy();
expect(screen.queryByText("hoge")).toBeNull(); // 要素がないことを検証
});
});
テストを走らせてみましょう。
$ npm run test
おわりに
以上、Unitテストを書く際の流れでした。