jestとReact Testing Libraryを使ってのテストコードの作成

# 概要
reactアプリでのテストコードの作成手順
※React Testing Libraryを使用
[参照したURL](https://qiita.com/ossan-engineer/items/4757d7457fafd44d2d2f)

# 目次
[1. テスト環境の準備](#1-テスト環境の準備)
[2. jestテストファイルの記述](#2-jestテストファイルの記述)
[3. メソッドのテスト](#3-メソッドのテスト)
[4. React Testing LibraryでReactコンポーネントをレンダリングする](#4-react-testing-libraryでreactコンポーネントをレンダリングする)
[5. getByText関数で要素を検索する](#5-getbytext関数で要素を検索する)
[6. その他の検索タイプ](#6-その他の検索タイプ)
[7. queryByを使って存在しない要素を検査する](#7-querybyを使って存在しない要素を検査する)
[8. findByを使って非同期で取得する](#8-findbyを使って非同期で取得する)
[9. イベントの発火をテスト](#9-イベントの発火をテスト)
[10. コールバックハンドラのテスト](#10-コールバックハンドラのテスト)
[11. 非同期処理(API呼び出し)のテスト](#11-非同期処理api呼び出しのテスト)


# 1. テスト環境の準備
開発環境の構築は以下参照
[Node.js+React+Herokuでアプリ開発環境を構築する](https://qiita.com/kubota_ndatacom/items/c62109ad6e570085af88)

create-react-appのコマンドでアプリ作成を行った場合jestとReact Testing Libraryは標準でインストールされている
Reactを自前でセットアップした環境の場合はjestとReact Testing Libraryをインストールする必要がある
[How to test React with Jest](https://www.robinwieruch.de/react-testing-jest/)


コマンドプロンプトで作成したアプリのfrontendフォルダに移動
「npm test」コマンドで、xxxx.test.jsファイルを対象にテストを実行してくれる

```
> frontend@0.1.0 test c:\heroku\jest-test\frontend
> react-scripts test
 PASS  src/App.test.js
  ● Console

/* テスト内容 */

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/Main.test.js (5.314 s)
  ● Console

/* テスト内容 */

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)


Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        6.459 s
Ran all test suites related to changed files.

Watch Usage
 ? Press a to run all tests.
 ? Press f to run only failed tests.
 ? Press q to quit watch mode.
 ? Press p to filter by a filename regex pattern.
 ? Press t to filter by a test name regex pattern.
 ? Press Enter to trigger a test run.
```

# 2. jestテストファイルの記述

describeブロックとtestブロックで構成される
describeブロック(テストスイート)が複数のテストケースの集合
testブロック(テストケース)が個別のテストを意味する
testブロックはdescribeに含めず単体で記述することも可能

React Testing Libraryを使用する場合はrenderでテスト要素をレンダリングして、screenで要素にアクセスしてテストを実行する。

```javascript:App.test.js
describe('テストスイート名称', () => {
  test('テストケース名称１', () => {
    render(<App />);
    screen.debug();
  });
  test('テストケース名称２', () => {

  });
});
test('個別テストケース名称', () => {

});
```

# 3. メソッドのテスト

jestのExpectを使った値の合致を判定するテストの記述方法の例
[参照](https://jestjs.io/ja/docs/expect
)
第一因数と第二因数の和を返すSumTest.jsがあり、そのメソッドの動作をテストする場合の記述

```javascript:SumTest.js
function SumTest(x, y) {
  return x + y;
}
export default SumTest;
```
```javascript:App.test.js
import SumTest from './SumTest';

describe('Test Case1', () => {
  test('sums up two values', () => {
    expect(SumTest(2, 4)).toBe(6);
  });
});
describe('Test Case2', () => {
  test('sums up two values', () => {
    expect(SumTest(2, 4)).toBe(7);
  });
});
```

SumTest(2, 4)の結果がtoBeの因数6と合致する'Test Case1'は成功
因数7と合致しないので'Test Case2'は失敗になる

# 4. React Testing LibraryでReactコンポーネントをレンダリングする

React Testing Libraryのrender関数でJSXをレンダリングし、screen.debug()でHTMLを出力する記述例


```javascript:App.js
import React,{ useState,useEffect } from 'react';

function App() {

  const [search, setSearch] = useState('');

  function handleChange(event) {
    setSearch(event.target.value);
  }

  return (
    <div className="container is-fluid">
      <Search value={search} onChange={handleChange}>
        検索
      </Search>
      <p>Searches for {search ? search : '...'}</p>
    </div>
  );
}

function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}

export default App;
```
```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('render test', () => {
    render(<App />);
    screen.debug();
  });
});
```

npm testの実行結果

```
> frontend@0.1.0 test c:\heroku\jest-test\frontend
> react-scripts test
  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            <label
              for="search"
            >
              検索
            </label>
            <input
              id="search"
              type="text"
              value=""
            />
          </div>
          <p>
            Searches for
            ...
          </p>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ render test (84 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.921 s
Ran all test suites related to changed files.
```

# 5. getByText関数で要素を検索する

getByText関数で要素を取得してexpectのtoBeInTheDocument()で存在をチェックする
※getByTextは合致する要素が複数ある場合、最初に合致したもののみ取得するので注意が必要

```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('render test', () => {
    render(<App />);

    /* 明示的にexpectのアサーションを記述 */
    expect(screen.getByText(/検索/)).toBeInTheDocument();

    /* 要素が取得出来なかった場合にエラーになるので、上記記述と同様のチェックとなる */
    screen.getByText(/検索/);

    screen.debug(screen.getByText(/検索/));

  });
});
```

「npm test」実行結果
```
> frontend@0.1.0 test c:\heroku\jest-test\frontend
> react-scripts test
  console.log
    <label
      for="search"
    >
      検索
    </label>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ render test (130 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.371 s
Ran all test suites related to changed files.
```

# 6. getByRole関数で要素を検索する

getByRole関数で要素を取得してexpectのtoBeInTheDocument()で存在をチェックする記述例
getByRole関数はaria-label属性で要素を取得する、HTML要素には暗黙的なroleがある(button要素のbuttonなど)
利用できないroleを選択すると代案をサジェストしてくれる
※getByTextと違い合致する要素が複数ある場合エラーになる、複数ある場合は「screen.getByRole('button', {name: 'TEST'});」のようにタグの中身を name として指定する

```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('render test', () => {
    render(<App />);

    expect(screen.getByRole('textbox')).toBeInTheDocument();

    screen.debug(screen.getByRole('textbox'));

  });
});
```

「npm test」実行結果
```
> frontend@0.1.0 test c:\heroku\jest-test\frontend
> react-scripts test
  console.log
    <input
      id="search"
      type="text"
      value=""
    />

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ render test (177 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.421 s
Ran all test suites related to changed files.
```

# 6. その他の検索タイプ

他にも、より要素を特定した検索タイプが存在する
getByLabelText: label for="search" 
getByPlaceholderText: input placeholder="Search"
getByAltText: img alt="profile"
getByDisplayValue: input value="JavaScript"



# 7. queryByを使って存在しない要素を検査する

getByで検査すると要素が存在しない場合エラーになってしまうので
要素が存在しない事を確認する場合queryByを使用する
※Role,LabelText,PlaceholderText等にも使用出来る

```javascript:App.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    /* JavaScriptが存在しない場合nullになるのでエラーにならない */
    expect(screen.queryByText(/JavaScript/)).toBeNull();

    /* JavaScriptが存在しない場合エラーになる */
    expect(screen.getByText(/JavaScript/)).toBeNull();
  });
});
```

# 8. findByを使って非同期で取得する

findByでまだ存在しないものの最終的に存在する要素について検査する
```javascript:App.js
import React,{ useState,useEffect } from 'react';

function getMessage() {
  return Promise.resolve('testMessage');
}

function App() {

  const [message, setMessage] = useState('');

  const [search, setSearch] = useState('');

  function handleChange(event) {
    setSearch(event.target.value);
  }

  useEffect(() =>{
    const loadMessage = async () => {
      const mes = await getMessage();
      setMessage(mes);
    };
    loadMessage();
  },[])

  return (
    <div className="container is-fluid">
      <Search value={search} onChange={handleChange}>
        検索
      </Search>
      <p>Searches for {search ? search : '...'}</p>
      {message && <div>Message:{message}</div>}
    </div>
  );
}

function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}

export default App;
```
```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('render test', async () => {
    render(<App />);

    /* queryByで検査するとまだ存在しないのでnullになる */
    expect(screen.queryByText(/Message:/)).toBeNull();

    screen.debug();

    /* findByで検査すると取得することが出来る */
    expect(await screen.findByText(/Message:/)).toBeInTheDocument();

    screen.debug();

  });
});
```

実行結果
初回のscreen.debug()ではMessageが生成されておらず、2回目のscreen.debug()ではMessageが生成されている
```
  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            <label
              for="search"
            >
              検索
            </label>
            <input
              id="search"
              type="text"
              value=""
            />
          </div>
          <p>
            Searches for
            ...
          </p>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            <label
              for="search"
            >
              検索
            </label>
            <input
              id="search"
              type="text"
              value=""
            />
          </div>
          <p>
            Searches for
            ...
          </p>
          <div>
            Message:
            testMessage
          </div>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ render test (116 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.361 s
Ran all test suites related to changed files.
```

# 9. イベントの発火をテスト

React Testing LibraryのfireEvent関数を使ってinputに値が入力された際の動作をテストする事が出来る

```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';

describe('App', () => {
  test('render test', async () => {
    render(<App />);

    await screen.findByText(/Message:/);

    /* Searches for inputValueは検出出来ずnullになる */
    expect(screen.queryByText(/Searches for inputValue/)).toBeNull();

    screen.debug();

    /* inputのvalueをinputValueに変更 */
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'inputValue' },
    });

    /* Searches for inputValueが検出される */
    expect(screen.getByText(/Searches for inputValue/)).toBeInTheDocument();

    screen.debug();

  });
});
```

実行結果
fireEvent実行前後のHTML構造が確認出来る
```
  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            <label
              for="search"
            >
              検索
            </label>
            <input
              id="search"
              type="text"
              value=""
            />
          </div>
          <p>
            Searches for
            ...
          </p>
          <div>
            Message:
            testMessage
          </div>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            <label
              for="search"
            >
              検索
            </label>
            <input
              id="search"
              type="text"
              value="inputValue"
            />
          </div>
          <p>
            Searches for
            inputValue
          </p>
          <div>
            Message:
            testMessage
          </div>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ render test (162 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.42 s
Ran all test suites related to changed files.
```

fireEvent.changeだとonchangeのイベントのみ発火するのでより正確にテストしたい場合はuserEventを使用する
[参照](https://testing-library.com/docs/ecosystem-user-event/)
※userEvent.typeはchangeイベントだけでなく、keyDown、keyPressやkeyUpイベントも発火させる
```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';

describe('App', () => {
  test('render test', async () => {
    render(<App />);

    await screen.findByText(/Message:/);

    /* Searches for inputValueは検出出来ずnullになる */
    expect(screen.queryByText(/Searches for inputValue/)).toBeNull();

    screen.debug();

    /* inputのvalueをinputValueに変更 */
    await userEvent.type(screen.getByRole('textbox'), 'inputValue');

    /* Searches for inputValueが検出される */
    expect(screen.getByText(/Searches for inputValue/)).toBeInTheDocument();

    screen.debug();

  });
});
```

# 10. コールバックハンドラのテスト

App.jsのSearchコンポーネントのonChangeの動作をチェックする

```javascript:App.js
export function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}
```

```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App, {Search} from './App';

describe('App', () => {
  test('callback handler test', () => {

    const mockFunc = jest.fn();

    render(
      <Search value="test" onChange={mockFunc}>
        検索
      </Search>
    );

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    expect(mockFunc).toBeCalledTimes(1);

  });
});
```

fireEventの実行でonChangeが動作し、mockFuncの呼ばれた回数が1回となる
※userEventはキー入力するごとに作動するので、同様に'JavaScript'とすると回数が10回となる

# 11. 非同期処理(API呼び出し)のテスト

fetchのモックを作成し、fetchの処理をテストする
fetch-mockとnode-fetchを使用するのでインストールする
```
$ npm install fetch-mock node-fetch
```

App.jsの<button>が押された時に動作するapiTestでのfetch('/api')の呼び出しをテストする
```javascript:App.js
import React,{ useState,useEffect } from 'react';

function App() {

  const [message, setMessage] = useState('');

  const apiTest = () => {
    fetch('/api')
      .then((res) => res.json())
      .then((data) => setMessage(data.message))
  }

  return (
    <div className="container is-fluid">
      {message && <div>Message:{message}</div>}
      <div>
        <button onClick={apiTest}>api-test</button>
      </div>
    </div>
  );
}

export default App;
```

```javascript:App.test.js
import React,{ useState,useEffect } from 'react';
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
import fetchMock from 'fetch-mock';

describe('App', () => {

  afterEach(() => {
    /* fetchMockをリセット */
    fetchMock.restore();
  });

  test('fech mock test', async () => {

    /* /apiでのgetのfetchに対してデータを返すmock */
    fetchMock
      .get('/api', {
        message: "backend-index-api-mock",
      });

    const mockFunc = jest.fn();

    render(<App />);

    await userEvent.click(screen.getByRole('button', {name: 'api-test'}));

    /* waitFor→getByでfetchの処理が終わるのを待つ */
    await waitFor(() => screen.getByText(/backend-index-api-mock/));
    /* findByでもOK */
    await screen.findByText(/backend-index-api-mock/);

    screen.debug();

  });

});
```

実行結果
```
  console.log
    <body>
      <div>
        <div
          class="container is-fluid"
        >
          <div>
            Message:
            backend-index-api-mock
          </div>
          <div>
            <button>
              api-test
            </button>
          </div>
        </div>
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

 PASS  src/App.test.js
  App
    √ fech mock test (183 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.118 s
Ran all test suites related to changed files.
```