jestとreact-testing-libraryでreactのテストをする

# はじめに
[1回目のReactを使用してWeb画面を作成する](https://qiita.com/mink0212/items/a53b285dd26f57b835b3)では、reactの簡単なサンプルと共に説明をまとめました。
[2回目のReduxとReduxToolkitを使用してReact内でデータを管理する](https://qiita.com/mink0212/items/add733e2a2a1dfbc3aed)では、reduxのの簡単なサンプルと共に説明をまとめました。
[3回目のReactのRedux内でaxiosを使用した通信をする](https://qiita.com/mink0212/items/b89388f9c255d7ef6833)では、axiosを使用してREST APIから情報を取得するサンプルと説明をまとめました。
今回は、jestとreact-testing-libraryで仮想DOMのテストをしてみます。

# 環境
 - node.js: v12.18.2
 - webpack: 4.44.1
 - React: 16.13.1
 - Redux: 7.2.1
 - axios: 0.19.2
 - jest: 26.3.0
 - react-testing-library: 10.4.8

# 環境作成
今までの続きとなります。
環境構築などはそちらを見てください。

## jest
テストをするために、その枠組みを提供するjestというライブラリを使用します。

### jestのインストール

```

yarn add --dev babel-jest react-test-renderer
npm install -g jest
```

## react-testing-library
reactが生成する仮想DOMの操作をするためにreact-testing-libraryというライブラリを使用します。

### react-testing-libraryのインストール

```

npm install --save-dev @testing-library/react
```

## axios-mock-adapter
axiosを使用して通信を行っていますが、テストのたびにREST APIのサーバを起動するのは手間なのでモックを使用してaxiosの処理を置き換えます。

### axios-mock-adapterのインストール

```

npm install --save-dev axios-mock-adapter
```

# テストソースを作成する
## テストソースを入れるフォルダの作成
テスト用ソースを入れるフォルダとしてルートフォルダの直下にtestフォルダを作成します。

```

project_root
├─dist   // ビルド後のファイルを格納 
├─public // htmlを格納
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─test    // reactのテストファイルを格納
```

## テストファイルの作成
jestは×××.spec.jsや×××.test.jsというファイルを読み込んで実行するため、それに沿ったファイル名にしてください。

``` Message.spec.js

import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

const mockAxios = new MockAdapter(axios);

mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
// テスト実行後にDOMをunmount, cleanupします
afterEach(cleanup)

describe("Messageテスト", () => {
    it("初期値:メッセージが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // メッセージが2つ(テキストとボタンにあるか)
        expect(messageRender.getAllByText("メッセージ")).toHaveLength(2);
        // こんにちはが1つ(ボタンにあるか)
        expect(messageRender.getAllByText("こんにちは")).toHaveLength(1);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("通信")).toHaveLength(1);
        // ボタンが2つあるか)
        expect(messageRender.getAllByRole("button")).toHaveLength(3);
    });

    it("メッセージを押すと入力したものが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // こんにちはが1つ(ボタンにあるか)
        const inputElement = messageRender.getByRole("textbox");
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // こんにちはが1つ(ボタンにあるか)
        const messageElement = messageRender.getAllByText("メッセージ");
        // 通信が1つ(ボタンにあるか)
        fireEvent.click(messageElement[0]);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
    });

    test("通信のテスト", async () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // 通信ボタンを取得
        const messageElement = messageRender.getAllByText("通信");
        // 通信ボタンをクリック。失敗すればエラーになる
        await fireEvent.click(messageElement[0]);
    });
});
```

### jestの形式
jestは指定されたフォーマットにしたがって書くことによってテストの実行をしてくれます。

``` Message.spec.js

インポート

// afterEach内の関数はテスト後に実行される
afterEach(cleanup)

// テストの塊
describe("Messageテスト", () => {
    // テスト
    it("初期値:メッセージが表示される", () => {
        ～～～　テストの内容　～～～　
    });
});
```

jestのフォーマットはこのような形になります。他にもテスト前やテストスイート前後にする処理を登録できます。


### 仮想DOMの生成
仮想DOMの生成には```react-testing-libraryのrender```を使用します。この中に、テストしたいアプリのソースを登録します。
今回はMessageコンポーネントのテストをしたいのでrenderに入れていますが、storeも使っているためそれも適用しています。

``` Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

        ～～～　省略　～～～　

        // 仮想DOMの生成
        const messageRender = render(<Provider store={store}><Message /></Provider>);
    });
```

### 仮想DOMの操作
生成した仮想DOMに対して```getByXXXやfindByXXX```といった関数を実行すると仮想DOM内の要素を抜き出すことができます。要素のイベントを発火させるためには```fireEvent.change(DOMElement);```で発火させます。
今回は入力するために```getByRole("textbox")```で抜き出して、値を入れた後に```fireEvent.change(inputElement);```で反映させています。
さらにボタンを押すために```getAllByText("メッセージ")```で抜き出した後に、メッセージという文字列が2つあり最初の要素がボタンであるため、そっちのイベントを発火させます。

``` Message.spec.js
        ～～～　省略　～～～　

        // 入力するテキストボックスを取得
        const inputElement = messageRender.getByRole("textbox");
        // テキストの入力と反映。
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // ボタンの取得
        const messageElement = messageRender.getAllByText("メッセージ");
        // ボタンのクリック
        fireEvent.click(messageElement[0]);
        // ボタンを押した結果を確認
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
        ～～～　省略　～～～　
```


### axiosのモックを生成
モックを使用するときは、```MockAdapter```を使用して、モックを生成した後に、モック内の処理を記載します。
今回はgetのリクエストを置き換えるので```onGet(URL).reply(httpステータス,レスポンスボディ)```を使用することにより、モック内の処理を定義します。

``` Message.spec.js
        ～～～　省略　～～～　
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

// mockの生成
const mockAxios = new MockAdapter(axios);

// mock内容を定義
mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
        ～～～　省略　～～～　
```

# テストソースの実行
プロジェクトルートでjestコマンドを実行することでテストできます。

```

jest
```

# 終わりに
厳密にテストする場合、action内のテストやstateのテストといった細かい単位でテストすることが可能になります。
それによってテスト結果がNGになった場合に、原因となる場所が限定されるので調査がしやすくなったり、修正後のテストの範囲を限定できたりと様々なメリットがあります。
しかし、そこら辺のテストをちゃんと書くにはReactのことを理解する必要がある上に時間がかかってしまうため、
Reactへのなじみのない人が実施すると時間だけがかかってしまい、テストソースがバグだらけとなったりしてしまいます。
厳密にテストするか大きな枠でテストするかは理解度と時間、テストの影響のトレードオフかなと思っています。