Reactをjestでテストする設定方法

# 1.はじめに
`create-react-app`で作成したappを、jestでテストする環境を作るのに、とても苦労したので、自分のためにも残します。
まだわからないことが多いですが、恥をさらして。:laughing:

試行錯誤してできた環境「jest-prac-app」を元に、もう一度いちから`create-react-app`して「jest-prac-app2」を作ってみたので、名前はそんな感じですが、たぶんほぼ必要十分です。

ソースはこちらです。

https://github.com/yo16/react_prac_test_jest

## 1.1.環境・バージョン
- Windows 10
- react: 18.2.0
- create-react-app: 10.2.3
  - `npm create-react-app --version`で確認

## 1.2.ディレクトリ構成
下記のパターン１で、フォルダ名は「test」です。
- 参考
  - [結局テストファイルはどこに置くのがベターなのか](https://zenn.dev/takumi_n/scraps/18b18819f92ce2)
  - [reactjs - Jest Folder Structure - Stack Overflow](https://stackoverflow.com/questions/62442797/jest-folder-structure)

```
jest-prac-app
├─ node_modules
├─ public
├─ src
├─ test
└─ package.jsonなどのファイル
```

# 2.テスト対象のコンポーネントを準備
## 2.1.create-react-app
```batch
$ npx create-react-app jest-prac-app2
$ cd jest-prac-app2
```
※ windowsですが、コマンドとわかりやすくするために $ を書いてます。

## 2.2. コンポーネント作成、配置
### 2.2.1. MyButton.jsを作成
クリックするとON/OFFが切り替わるボタンです。

さっそくここで、のちのテスト時にハマった点を１つ。１行目の`React`は省略できるんですが、入れないとテスト時にエラーになるので入れています。
```JavaScript:jest-prac-app/src/MyButton.js
import React, { useState } from "react"

const MyButton = () => {
    const [isOn, setIsOn] = useState(false);

    const handleClick = () => {
        setIsOn((prevState) => !prevState);
    };

    return (
        <button onClick={handleClick}>{isOn?"ON":"OFF"}</button>
    );
}

export default MyButton;
```

### 2.2.2. index.jsからAppを外して、代わりにMyButtonを入れる
reportWebVitals関連は、今はいらないので消します。
```javascript:jest-prac-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import MyButton from './MyButton';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <MyButton />
  </React.StrictMode>
);
```

### 2.2.3. 不要ファイルを削除
App.js、App.test.js、App.cssは、放っておいてもいいけど、テストで動いたりするとめんどくさいので、ファイル削除。

## 2.3. appの動作確認

```batch
$ npx run start
```
ブラウザに出て、デフォルトOFFから、クリックするとON、OFFと切り替わる。htmlはbuttonタグです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/807ed321-1ade-9f03-d674-9aad81e04dfb.png)

# 3. テスト環境の設定＜本題＞:muscle:
さて、テスト環境。
## 3.1. package.json
テストをjestで行うために、"test"の行のテスト用コマンドを修正。
```json:jest-prac-app/package.json
{
  "name": "jest-prac-app2",
  ～　略　～
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "jest",
    "eject": "react-scripts eject"
  },
  ～　略　～
}
```
## 3.2. jest.config.jsを作成
- `testMatch`：テスト用スクリプトの場所
  - ディレクトリ構成のキモ。
- `moduleNameMapper`：importのファイルは、`@`で始まってたら、rootDirを見てねという設定
  - 正規表現で置換するんですね。`()`の部分を`$1`で使う形。
- `setupFilesAfterEnv`：最初からある`/src/setupTest.js`を参照する設定
  - 入れないと、`TypeError: expect(...).toHaveTextContent is not a function`という、関係性がわからない変なエラーが出る。
- `testEnvironment`：`jsdom`は、今はわからない:frowning2:（要調査）
  - とあるエラーが出たときに、:robot:ChatGPTさんに教えてもらった。
```javascript:jest-prac-app/jest.config.js
module.exports = {
    testMatch: ["<rootDir>/test/**/*.test.js"],
    moduleNameMapper: {
      "^@/(.*)$": "<rootDir>/$1",
    },
    setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],
    testEnvironment: 'jsdom',
};
```
## 3.3. Babel環境の設定
こちらも、今は十分理解できてないです。:frowning2:（要調査）
> Babelのプリセットを設定します。（中略）Babelは、ES.nextの構文とJSXを、ブラウザが解釈できるコードに変換します。（[O'Reilly Japan - Reactハンズオンラーニング 第2版](https://www.oreilly.co.jp//books/9784873119380/)）
```batch
$ npm install @babel/preset-env @babel/preset-react
```
babel.config.jsを作成します。
```javascript:jest-prac-app/babel.config.js
module.exports = {
    presets: ["@babel/preset-env", "@babel/preset-react"],
};
```

## 3.4. @testing-library/user-eventをアップデート
あとで書くテストスクリプト中の、`const user = userEvent.setup();`で、`TypeError: _userEvent.default.setup is not a function`というエラーが出ました。そんなバカなって感じですが、latestを入れれば解決。
- 参考
  - [_userEvent.default.setup is not a function · Issue #839 · testing-library/user-event](https://github.com/testing-library/user-event/issues/839)
    - https://github.com/testing-library/user-event/issues/839#issuecomment-1130714767

どこかで相性問題がおこるかもしれないと目にしたので、/reactもlatestを入れます。
```batch
$ npm install @testing-library/react@latest @testing-library/user-event@latest
```

ちなみにバージョンは、私の環境では下記でした。
```json:jest-prac-app2/package.json（抜粋）（アップデート前）
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
```
```json:jest-prac-app2/package.json（抜粋）（アップデート後）
    "@testing-library/react": "^14.2.0",
    "@testing-library/user-event": "^14.5.2",
```

# 4. テストスクリプトの作成と実施
## 4.1. テストスクリプトの作成
１行目の、Reactは、コンポーネントと同様、ないとエラーが出ます。

テストの書き方などの詳細は割愛します。なお`act()`についてまだ理解不足で、おそらく書き方がよくない。でも理解を避けては通れないと感じているところです。
- 参考
    - [React のテストを書いてたら act で囲んでよーって言われたとき - Mitsuyuki.Shiiba](https://bufferings.hatenablog.com/entry/2021/11/18/015809)（:frowning2:よく読む）
```javascript:jest-prac-app2/test/MyButton.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from 'react-dom/test-utils';
import MyButton from "@/src/MyButton";

describe("myButtonのテスト", () => {
    test("ボタンをクリックするとON/OFFが切り替わる", async () => {
        // まず描画
        act(() => {
            render(<MyButton />);
        });

        // ユーザーを想定
        const user = userEvent.setup();

        // 初期状態をテスト
        const myButton = screen.getByRole("button");
        expect(myButton).toHaveTextContent("OFF");

        // クリック
        await act(async () => {
            await user.click(myButton);
        });

        // 今度はONになる
        expect(myButton).toHaveTextContent("ON");

    });

    it("itでもいいっていうだけのテスト", async () => {
        // まず描画
        render(<MyButton />);

        // 初期状態をテスト
        const myButton = screen.getByRole("button");
        expect(myButton).toHaveTextContent("OFF");
    });

    test("描画されてすぐはOFFと表示されている", () => {
        // 描画
        const view = render(<MyButton />);

        // スナップショットを取る
        expect(view.container).toMatchSnapshot();
    });
});
```

## 4.2. テスト実施
```batch
$ npm run test

> jest-prac-app2@0.1.0 test
> jest

 PASS  test/MyButton.test.js
  myButtonのテスト
    √ ボタンをクリックするとON/OFFが切り替わる (174 ms)
    √ itでもいいっていうだけのテスト (13 ms)
    √ 描画されてすぐはOFFと表示されている (7 ms)

 › 1 snapshot obsolete.
   • MyButtonのテスト スナップショットでテスト 1
Snapshot Summary
 › 1 snapshot obsolete from 1 test suite. To remove it, run `npm test -- -u`.
   ↳ test/MyButton.test.js
       • MyButtonのテスト スナップショットでテスト 1

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   1 obsolete, 1 passed, 1 total
Time:        3.171 s
Ran all test suites.
```
OK!
`jest-prac-app2/test/__snapshots__`に、スナップショットのファイルもできてました。

# 5. おわりに
バージョンアップが速いせいかなんかわかりませんが、ググっても正解なんだかそうでないんだかわからないし、聞ける先輩もいない中、試行錯誤で解を得られたことはまずよかったです。何度か諦めかけた・・・。やり始めてからこの記事を書き終えるまで、大体5時間くらいかかりました。

「create-react-app」で道を開拓して、「create-react-app2」できれいな道をもう一度通るという方法でしたが、それでもすんなりとはいかないです。自分が通った道（やったこと）を忘れてるだけなんですが。:joy:

さて。まだ知識不足のところはありつつも、テストの環境はできるようになったっぽいので、テストをエンジョイしたいです。みなさんも、良いテストコーディングを！:sunny: