Reactのコンポーネントをカバレッジが100％になるまでテストする

# 概要

今回は、前回まで作って来たReactコンポーネントをテストするコード作成します。

（いままでの内容が気になる方は、ぜひご覧ください）


- 1：[ChatGPTに3回質問して「文字の類似度」のサジェストするプログラム作成](https://qiita.com/ishi720/items/3f58685ce6230c0e4d7d)
- 2：[「文字の類似度」からポケモン名をサジェストするコンポーネントを実装【React】](https://qiita.com/ishi720/items/96a86f7a4ebd2d584859)
- 3：[【React】JavaScriptからTypeScriptに移行してみた](https://qiita.com/ishi720/items/96a86f7a4ebd2d584859)

### カバレッジとは

カバレッジとはコード網羅率のことで、プログラムがテストされた割合の指標です。

Reactの標準機能として、カバレッジを出力する機能が搭載されていたので、こちらも利用していきます。

# 環境

- react: 18.2.0
- jest: 27.5.2
- jest-dom: 5.17.0

# テストするプログラム

プログラムの詳細は、前回説明済みなので省略します。

<details>
<summary>「文字の類似度」からポケモン名をサジェストするコンポーネント</summary>

```ts
import React, { useState, useEffect } from 'react';
import './App.scss';
import { pokeNameList } from './pokeNameList';

const App = () => {
  return <SuggestForm />;
};

const SuggestForm = () => {
  const [value, setValue] = useState<string>('');
  const suggestNum: number = 15 ;
  const wordList: string[] = pokeNameList;
  const [suggestions, setSuggestions] = useState<string[]>([]);

  useEffect(() => {
    const suggestions: string[] = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
    setSuggestions(suggestions);
  }, [value, wordList, suggestNum]);

  // イベント
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const updatedValue = event.target.value;
    setValue(updatedValue);
  };
  const handleClickWord = (word: string) => {
    setValue(word); // クリックされたワードを検索ボックスにセットする
  };
  /**
  * レーベンシュタイン距離の計測
  * @param {string} str1 文字列1
  * @param {string} str2 文字列2
  * @return {array} レーベンシュタイン距離の計測結果
  */
  const levenshteinDistance = (str1: string, str2: string) => {
    const len1 = str1.length;
    const len2 = str2.length;
    
    const dp: number[][] = [];
    
    for (let i = 0; i <= len1; i++) {
      dp[i] = [];
      for (let j = 0; j <= len2; j++) {
        if (i === 0) {
          dp[i][j] = j;
        } else if (j === 0) {
          dp[i][j] = i;
        } else {
          dp[i][j] = 0;
        }
      }
    }
    
    for (let i = 1; i <= len1; i++) {
      for (let j = 1; j <= len2; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
      }
    }
    
    return dp[len1][len2];
  };
  /**
  * 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
  * @param {string} searchWord 検索文字列
  * @param {array} wordList 検索対象の一覧
  * @param {number} n 返却する文字列の数
  * @return {array} 評価順のデータ
  */
  const getSuggest = (searchWord: string, wordList: string[], n: number) => {
    return wordList
      .sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
      .slice(0, n);
  };
  /**
  * ひらがなをカタカナに変換する
  * @param {string} s ひらがな文字列
  * @return {string} カタカナ文字列
  */
  const hiraganaToKatakana = (s: string) => {
    return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
      return String.fromCharCode(match.charCodeAt(0) + 0x60);
    });
  };
  return (
    <div>
      <div>
        <label htmlFor="pokemonSearch">ポケモン名　</label>
        <input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
      </div>
      <div id="wordList">
        <p>もしかして...</p>
        {suggestions.map((word, index) => (
          <span
            key={index}
            className="keyword"
            onClick={() => handleClickWord(word)} 
          >
            {word}
          </span>
        ))}
      </div>
    </div>
  );
};

export default App;
```
</details>

# テスト追加①

### テスト内容

まず、サジェストがちゃんと15個表示されているかテストします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/40489014-387e-88f9-b701-ee47cada736b.png)

### テストコード実装


```ts
import { render, fireEvent, screen } from '@testing-library/react';
import App from './App';

describe('App component', () => {
  test('サジェストの個数のテスト', () => {
    render(<App />);
    // サジェストの数を取得
    const showSuggestEle = document.getElementsByClassName('keyword');
    const showSuggestCount = showSuggestEle.length;

    // サジェストの数のテスト
    expect(showSuggestCount).toBe(15);
  });
});
```

### テスト実行

`yarn test`でテストを実行できます。
`--coverage`をつけることでカバレッジを出力することができます。

```bash
$ yarn test --coverage
yarn run v1.22.19
$ react-scripts test --coverage
PASS src/App.test.tsx
  App component
    √ サジェストの個数のテスト (48 ms)

----------|---------|----------|---------|---------|----------------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|----------------------------
All files |   71.73 |    16.66 |   66.66 |   71.42 |
 App.tsx  |   71.73 |    16.66 |   66.66 |   71.42 | 22-23,26,45-48,54-58,84,99
----------|---------|----------|---------|---------|----------------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.402 s
Ran all test suites related to changed files.
```

1つのテストが`passed`になっているので、テスト自体はうまくいっているようです。


### カバレッジ

カバレッジは、`/coverage/Apptsx.html`に作成されます。

出力されたカバレッジは以下の通りになります。
テストが足りないため、`10.41%`と網羅度は低いのでテストを追加したほうがいいようです。

![_C__repos_lll_my-app_coverage_lcov-report_App.tsx.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/6f660fe4-287f-2d19-ce1b-7d84608f6ad8.png)

# テスト追加②

### テスト内容

文字を入力し、表示されたタグをクリックし、インプットタグを更新されるかテストします。

実際に操作すると以下のようになります。

![Animation4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/b185550f-162c-42cd-a8ce-ed1d51b00dc8.gif)

### テストコード実装

テストケースひとつだけだと心もとないので、以下の２つのケースを追加しました。
- サジェスト結果のテスト(ぴかちう → ピカチュウ)
- サジェスト結果のテスト(てらぱらす → テラパゴス)

```ts
import { render, fireEvent, screen } from '@testing-library/react';
import App from './App';

describe('App component', () => {
  test('サジェストの個数のテスト', () => {
    render(<App />);
    // 特定のクラス名を持つ要素を取得
    const showSuggestEle = document.getElementsByClassName('keyword');
   
    // サジェストの数を取得
    const showSuggestCount = showSuggestEle.length;

    // サジェストの数のテスト
    expect(showSuggestCount).toBe(15);
  });

  test('サジェスト結果のテスト(ぴかちう → ピカチュウ)', () => {
    render(<App />);

    // inputタグにぴかちうを入力
    const searchInput = screen.getByLabelText('ポケモン名');
    fireEvent.change(searchInput, { target: { value: 'ぴかちう' } });

    // サジェストの1つ目をクリック
    const elementsWithKeywordClass = document.getElementsByClassName('keyword');
    fireEvent.click(elementsWithKeywordClass[0]);

    // inputタグがピカチュウに更新されるかテスト
    const afterInput = screen.getByLabelText('ポケモン名') as HTMLInputElement;
    expect(afterInput.value).toBe('ピカチュウ');
  });

  test('サジェスト結果のテスト(てらぱらす → テラパゴス)', () => {
    render(<App />);

    // inputタグにてらぱらすを入力
    const searchInput = screen.getByLabelText('ポケモン名');
    fireEvent.change(searchInput, { target: { value: 'てらぱらす' } });

    // サジェストの1つ目をクリック
    const elementsWithKeywordClass = document.getElementsByClassName('keyword');
    fireEvent.click(elementsWithKeywordClass[0]);

    // inputタグがテラパゴスに更新されるかテスト
    const afterInput = screen.getByLabelText('ポケモン名') as HTMLInputElement;
    expect(afterInput.value).toBe('テラパゴス');
  });
});

```

### テスト実行

```bash
PASS src/App.test.tsx
  App component
    √ サジェストの個数のテスト (48 ms)
    √ サジェスト結果のテスト(ぴかちう → ピカチュウ) (101 ms)
    √ サジェスト結果のテスト(てらぱらす → テラパゴス) (72 ms)

A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |
 App.tsx  |     100 |      100 |     100 |     100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.867 s
Ran all test suites related to changed files.
```

### カバレッジ

網羅度が100%になったので、すべての処理をテストすることができました。（気持ちいいですよね）


![_C__repos_lll_my-app_coverage_lcov-report_App.tsx.html (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/2a06f057-b814-20ea-f9b2-ad9d89015e80.png)

# 終わりに

今回は、Reactのユニットテストを行うことができました。
こんなに簡単にWEB画面のテストができてしまうのは、すごいと思います。（実はWEB画面のテストは、初めてでした

ここまで読んでいただきありがとうございました。