React Testing Libraryで要素を取得する方法

# はじめに
ReactTestingLibrary(RTL)を利用する際に、要素を取得する方法をまとめました。本記事では単純にレンダリングのテストを行っているだけに留まりますが、RTLを使ってテストを始める取っ掛かりとして参考になればと思います。

**対象読者**
Reactで開発してて、これからテストを導入したい人
サンプルリポジトリはこちらです(参考程度に)

https://github.com/10mi8o/sample_qiita/tree/main/src

# ① テストファイルの中でコンポーネントをレンダリングする。
RTLは、Render結果であるDOM構造をテストするので実際のユーザー行動に近いテストをし易いのが特徴です。

コンポーネントのユニットテストをするには、まずはテストファイル内にコンポーネントをimportしてレンダリングしてあげる必要があります。そのためには、`render`関数を使用します。

## render
任意のJSXを受け取ってレンダリングする関数です。
render関数実行後、Reactコンポーネントにアクセスできるようになります。

これがコンポーネントのユニットテストを書く際の初めの一歩です。

```js:render
describe("RenderingTest", () => {
  test("正しく表示されてること", () => {
    render(<RenderTest />);
  });
});
```

## debug
render関数でReactコンポーネントにアクセスできるようにしたら、debug関数によってコンポーネントのHTML構造を確認することができます。実際に確認してみましょう。

```js:debug
screen.debug()
```

ショートハンドで書くこともできます。

```js:ショートハンド
describe("RenderingTest", () => {
  test("正しく表示されてること", () => {
    const { debug } = render(<RenderTest />);
    debug(); // コンソールにレンダリング結果を出力
  });
});
```

debug関数を実行すると以下のような結果が得られます。

```terminal:出力結果
  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>
```


単一または複数の要素を抽出してデバッグすることもできます。

```js:単一/複数要素のデバッグ
// 単一要素
screen.debug(screen.getByText("これはレンダリングテストのサンプルです。"))
// 複数要素
screen.debug(screen.getAllByText("これはレンダリングテストのサンプルです。"))
```

これでRTLから見えるHTML構造が確認できたので、これを元にテストを書いていく準備ができました。
次に、検証したい要素を取得する必要があります。

# ② 検証対象の要素を取得する
レンダリングした後は、RTLに用意された検索関数を使って、要素を取得していきます。
要素の選択をする際は、`screen.[query]`で取得できます。

```js:要素取得の一例
screen.getByRole("heading");
```

クエリのタイプは以下の通りです。これらの違いは、**要素が見つからない場合にエラーをスローするか**、**Promiseを再試行するか**で、状況に応じて適切なクエリを使って要素を取得する必要があります。

## 単一要素を取得するクエリ
[公式ドキュメント](https://testing-library.com/docs/queries/about/#types-of-queries)にもまとめられてますが、一つずつ見ていきましょう。

#### getBy
クエリに一致するノードを返します。
一致する要素がない場合、または複数の一致が検出された場合にもエラーを投げます。
**要素を取得する際の最も基本的なクエリーになるので、まずはgetByを使うことを検討します。**

#### queryBy
クエリに一致するノードを返し、一致する要素がない場合、`null`を返します。
複数の一致が検出された場合はエラーを投げます。
**要素がないことをテストしたい時に役に立ちます。**

#### findBy
指定されたクエリに一致する要素が見つかったときに、resolveを返します。
デフォルトのタイムアウト後に、要素が見つからない場合、または複数の要素が見つかった場合、rejectを返します。
**ボタンを押下後、DOMの変更を待って要素を取得したい等の場合に利用します。**

##### ※単一要素を取得するクエリの注意点
これらは**クエリーに一致する要素が複数見つかった場合はエラーを投げる**ので、その場合は後述する「複数要素を取得する際のクエリ」を使う必要があります。

## 複数要素を取得するクエリ
#### getAllBy
クエリに一致するすべてのノードの配列を返します。
一致する要素がない場合はエラーを投げます。

#### queryAllBy
クエリに一致するすべてのノードの配列を返します。
一致する要素がない場合は、空配列を返します。

#### findAllBy
指定されたクエリに一致する要素が見つかった場合に、resolveを返します。
デフォルトのタイムアウト後に要素が見つからなかった場合、rejectを返します。


##実際に要素を取得してみる
上記のクエリーを使って実際に要素を取得してみます。
以下に単純なコンポーネントがあります。

```jsx:SampleComponent
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

以下のリンクで取得できる要素を確認することができます。

https://github.com/A11yance/aria-query#elements-to-roles

```js:使用例:コンポーネントの中にheadingタグが存在すること
expect(screen.getByRole("heading")).toBeTruthy();
```

上記でheadingタグを抽出することはできますが、例えば、`h1`と`h2`がコンポーネントの中に存在する場合はどうするでしょうか？

この場合、第二引数に以下のoptionsを渡すことで、特定要素を抽出することができます。
以下に例を示します。

```js
  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,
  }
```

```js:optionsを指定して要素をフィルタリングする方法例
// 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
フォームフィールドを抽出する際に適したメソッドです。
例えばコンポーネントの中に以下の要素があった場合

```jsx:SampleComponent
<label htmlFor="count">Count</label>
<input id="count" type="text" placeholder="Enter" />
```

`getByLabelText`を使用すると以下の結果が得られます。

```js:testファイル
debug(screen.getByLabelText("Count"));
```
![スクリーンショット 2021-11-27 11.36.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/3fb03190-ae1c-0e2a-7af6-0cd27e1e3b28.png)

#### getByPlaceholderText

フォームフィールドを取得したいけど、`label`が定義されていないというケースもあると思います。
その際は`getByPlaceholderText`を使って取得しましょう。これでも、consoleには上記と同じ結果が出力されます。

```js:testファイル
 debug(screen.getByPlaceholderText("Enter"));
```

#### getByText
divやspan、pタグの要素を見つける際に役立ちます。
コンポーネントの中に以下の要素があった場合、、、

```jsx:SampleComponent
<div>
  これはレンダリングテストのサンプルです。<span>forStudy</span>
</div>
```

`getByText`を使用すると以下の結果が得られます。
親要素のテキストで抽出すると子要素も取得されるので、子要素のみ取得したい時は、対象のテキストを引数に渡しましょう。

```js:testファイル
debug(screen.getByText("これはレンダリングテストのサンプルです。"));
debug(screen.getByText("forStudy"));
```
![スクリーンショット 2021-11-27 11.50.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/4cfc0596-717c-3fa9-9b7a-0cc14535da0a.png)


#### getByDisplayValue
フォーム要素に入力されてる値から要素を取得するメソッドです。
値の入力をテストする際に役立ちます。

```jsx:SampleComponent
<input id="count" type="text" placeholder="Enter" value="defaultValue" />
```

```js:testファイル
debug(screen.getByDisplayValue("defaultValue"));
```
![スクリーンショット 2021-11-27 11.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/61b48548-2688-dbea-f733-9d616bde4947.png)



#### getByAltText
主にimgタグ、その他inputタグ、areaタグを取得する際に使用します。

```jsx:SampleComponent
<div>
  <img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="ReactLogo" />
</div>
```

```js:testファイル
debug(screen.getByAltText("ReactLogo"));
```
![スクリーンショット 2021-11-27 12.09.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/9a18956e-3c5d-11e9-3ab0-aa28f30354cf.png)


# ③ 取得した要素を検証する

レンダリング結果をテストしているだけなので、非常にシンプルなテストですが、以下がテストコードのサンプルです。

```js:TestSample/Render.test.js
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(); // 要素がないことを検証
  });
});
```

テストを走らせてみましょう。

```terminal
$ npm run test
```

コンソールに以下のようにテスト結果が表示されます。
![スクリーンショット 2021-11-27 12.44.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/4b568bbf-f054-248a-7731-e36836bbd7ed.png)

# おわりに

以上、Unitテストを書く際の流れでした。