React Testing Library (RTL) の要素を取得する API の使い分け

# はじめに
この記事では、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 が利用できるように設定しておきます。

https://qiita.com/Yasushi-Mo/items/0a29df164b02bd7d78e6

# 要素を取得する 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``` を利用します。

```Home.tsx
export const Home = () => {
  return <div>You are home</div>;
};
```

```Home.test.tsx
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();
  });
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/1b45dc43-9b8e-f813-fc36-27bde42f8e5e.png)

## ```queryBy (queryAllBy)``` のユースケース
```queryBy``` は、要素が存在しない場合、```null``` を返す（```queryAllBy``` の場合、```[]```）ため、存在しない要素に対して利用されます。 

例えば、以下のように ```props``` の値によって、テキストの表示・非表示が変わる要素があるとします。

```Home.tsx
export const Home = ({ authorized }: { authorized: boolean }) => {
  return (
    <>
      {authorized && <div>You are authorized</div>}
    </>
  );
};
```

この要素のテキストが表示されている場合のテストは、```getByText``` を利用すれば、問題なくテストできます。
```Home.test.tsx
describe("Home component", () => {
  test("when authorized", async () => {
    render(<Home authorized={true} />, { wrapper: BrowserRouter });

    expect(screen.getByText(/you are authorized/i)).toBeInTheDocument();
  });
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/5195e9c8-02a1-b09f-fd97-5b02d54bd2e3.png)

一方、テキストが非表示の場合のテストは、```getByText``` を利用すると、エラーになってしまいます。

```Home.test.tsx
test("when not authorized", async () => {
  render(<Home authorized={false} />, { wrapper: BrowserRouter });

  expect(screen.getByText(/you are authorized/i)).not.toBeInTheDocument();
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/64a4da79-25e0-f9d2-1d83-50e52d90441e.png)

そのため、```getByText``` の代わりに ```queryBy``` を利用します。

```Home.test.tsx
test("when not authorized", async () => {
  render(<Home authorized={false} />, { wrapper: BrowserRouter });

  expect(screen.queryByText(/you are authorized/i)).not.toBeInTheDocument();
});
```

エラーが発生することなく、非表示であることのテストができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/bde6ccdc-9907-c705-c51f-e028059d00fa.png)

## ```findBy (findAllBy)``` のユースケース
```findBy (findAllBy)``` は、要素を非同期に取得することができるため、state の値が変わったことによるUIの変化や非同期APIのレスポンスが返ってきた後のUI表示などに利用できます。

例えば、以下のようにボタンをクリックすることでボタン上のテキストが変わる要素があるとします。

```Home.tsx
import { useState } from "react";

export const Home = () => {
  const [buttonText, setButtonText] = useState("button");
  return <button onClick={() => setButtonText("clicked")}>{buttonText}</button>;
};
```
ボタンクリック後にテキスト表示が変化することをテストする際、```getByText``` を利用するとエラーになってしまいます。
```Home.test.tsx
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();
  });
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/41969b88-220d-770a-7ccf-fbe1ba8eaab5.png)

一方、```findByText``` を ```await``` と一緒に利用し、非同期に要素を取得すれば、エラーなく、テストをすることができます。
```Home.test.tsx
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();
  });
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/fffc625e-0046-648a-c3dc-d217e6dde913.png)

:::note
以下の書き方であれば、 ```getByText``` を利用してもエラーなく、テストをすることができます。

方法1. クリック処理に ```await``` をつける
```Home.test.tsx
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``` を利用する
```Home.test.tsx
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.

https://testing-library.com/docs/guiding-principles/

こちらの思想にもとづき、クエリの方法（```By...```）は優先度は以下の順番で利用することが推奨されています。

1. Queries Accessible to Everyone
    1. ```getByRole```
    1. ```getByLabelText```
    1. ```getByPlaceholderText```
    1. ```getByText```
    1. ```getByDisplayValue```
2. Semantic Queries
    1. ```getByAltText```
    2. ```getByTitle```
3. Test IDs
    1. ```getByTestId```

# 参照
- [About Queries](https://testing-library.com/docs/queries/about)
- [Guiding Principles](https://testing-library.com/docs/guiding-principles/)
- [【react-testing-library】queryBy, findByはどう使えばいいのか？](https://dotengineerblog.net/how-to-use-react-testing-library-queries/)
- [testing libraryでのgetBy, queryBy, findByの違いについて](https://zenn.dev/moneyforward/articles/20230329-query)