react-testing-libraryを使ったテストの始め方

### 準備
```zsh
// viteでプロジェクト作成
$ npm create vite

// 必要なライブラリのインストール
$ npm install -D jest @types/jest ts-jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

// Jestの設定ファイルの作成 -> jest.config.jsが作成される
$ npx ts-jest config:init
```

<details>
<summary>package.json</summary>

```json
{
  "name": "react-testing-library",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "test": "jest",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.1.4",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "^14.5.1",
    "@types/jest": "^29.5.7",
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}
```
</details>

<details>
<summary>必要ライブラリ</summary>

- Jest : https://jestjs.io/ja/
- jest-environment-jsdom : https://www.npmjs.com/package/jest-environment-jsdom
- testing-library : https://testing-library.com/
- testing-library/react : https://www.npmjs.com/package/@testing-library/react
- testing-library/jest-dom : https://github.com/testing-library/jest-dom
- testing-library/user-event : https://www.npmjs.com/package/@testing-library/user-event
</details>

#### jest.config.jsの追記
1. 初期設定だとCommonJSの記述方法だが、ESmodulesで記述したいため export default に変更する
2. 初期設定だとtestEnvironmentがnodeで指定されているが、ブラウザでテストしたいためjest-environment-jsdomに変更する
3. @testing-library/jest-domのsetupを読み込むためにsetupFilesAfterEnvを指定する
    - 参考 : https://github.com/testing-library/jest-dom#usage
    - ルートディレクトリに `jest-setup.js` を作成
```JavaScript:jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */

// module.exports = {
//   preset: 'ts-jest',
//   testEnvironment: 'node',
// };

export default {
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterEnv: ["./jest-setup.js"],
};
```

```JavaScript:jest-setup.js
import "@testing-library/jest-dom";
```

#### tsconfig.jsonの追記
- プロジェクト内で CommonJSのモジュールの読み込みによる警告アラートを出さないようにする設定
- 型エラーによるアラートを出さないようにする設定
```json:tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "types": ["@testing-library/jest-dom"]
  }
}
```

#### jestを走らせるnpm scriptの追記
- `"test": "jest"` を追加 -> `npm run test` でjestが走る
```json:package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "test": "jest",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
}
```

### UIテスト
:::note
テストの観点
1. 対象のコンポーネントがレンダリングされているか
2. 対象のコンポーネントが想定通りの要素を持っているか
3. 対象のコンポーネントが想定通りのアクション・挙動をするか
:::

<details>
<summary>ボタンコンポーネントのUIをテストする例</summary>

- テストしたいコンポーネントをインポートする
- `testing-library/react` の `render関数` を使ってテスト対象を指定する
- `testing-library/react` の `screen関数` を使ってテスト対象がレンダリングされた際のDOMを取得する
- expect()を使ってテスト内容を記述する
```JSX:Button.test.tsx
import { render, screen } from "@testing-library/react";

import Button from "./Button";

describe("Button", () => {
  it("ボタンコンポーネントのテスト", () => {
    // render() で実際にレンダリングされたコンポーネントを引数の中に入れる
    render(<Button label="ボタン" onClick={() => alert("click!")} />);

    // screenオブジェクトにはDOMの要素を取得する関数が用意されている -> role属性を取得できるgetByRole()を使ってボタンのDOMを取得する
    const element = screen.getByRole("button");

    // toBeInTheDocument()を使って、DOMが存在するかどうかを確認する
    expect(element).toBeInTheDocument();

    // toHaveTextContent()を使って、DOMのテキストが想定通りかどうかを確認する
    expect(element).toHaveTextContent("ボタン");
  });
});
```
</details>

<details>
<summary>フォームコンポーネントのUIと動作をテストする例</summary>

- テストしたいコンポーネントをインポートする
- `jest.spyOn` を使ってブラウザの挙動をモック化する
- `testing-library/react` の `render関数` を使ってテスト対象をレンダリングする
- `testing-library/react` の `screen関数` を使ってテスト対象がレンダリングされた際のDOMを取得する
- `@testing-library/user-event` の `userEvent` オブジェクトを使って、クリックした時の動作を登録する
    - **ボタンをクリックする、入力するなどの処理は非同期処理となるため、async/awaitをつける**
- expect()を使ってテスト内容を記述する
- spyOnを使ってテストをモック化した場合、テストが完了したらspyをクリアする
```JSX:Button.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import Form from "./Form";

// ユーザーイベントをテストするために初期設定としてイベントのインスタンスを作成する
const user = userEvent.setup();

describe("Form", () => {
  it("初期状態ではテキストは空欄", () => {
    render(<Form />);

    // inputフォームのDOMを取得する
    const inputEle = screen.getByPlaceholderText("Enter text");

    // toBeInTheDocument()を使って、DOMが存在するかどうかを確認する
    expect(inputEle).toBeInTheDocument();
    // toHaveValue()を使って、DOMのテキストが想定通りかどうかを確認する
    expect(element).toHaveValue("");
  });

  // ユーザーイベントは非同期関数となるためasync/awaitを使ってテストする
  it("入力されたテキストがサブミットされる", async () => {
    /*
      ボタンがクリックされたときwindowアラートを表示させるため、alert()をモック化する
      spyOn関数の第一引数には対象のオブジェクト、第二引数にはモック化する関数を指定する
      mockReturnValue()を使って、モック化した関数の戻り値を指定する
      今回のようにはwindow.alert()は何も返さないため、mockReturnValue()の引数は空となる
    */
    const alertSpy = jest.spyOn(window, "alert").mockReturnValue();

    render(<Form />);

    // submitボタンのDOMを取得する
    const submitEle = screen.getByRole("button");
    // ユーザーがサブミットボタンをクリックする動作を登録する
    // click()の第一引数には対象のDOM要素を指定する
    await user.click(submitEle);
    // alert()が呼ばれて、期待している文字列が表示されたかどうかを確認する
    expect(alertSpy).toHaveBeenCalledWith("submitted: テスト");
    // テストが終わったらspyをクリアする
    alertSpy.mockClear();
  });
});

```
</details>