LoginSignup
2
2

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

Last updated at Posted at 2024-01-08

概要

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

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

カバレッジとは

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

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

環境

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

テストするプログラム

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

「文字の類似度」からポケモン名をサジェストするコンポーネント
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;

テスト追加①

テスト内容

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

image.png

テストコード実装

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をつけることでカバレッジを出力することができます。

$ 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

テスト追加②

テスト内容

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

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

Animation4.gif

テストコード実装

テストケースひとつだけだと心もとないので、以下の2つのケースを追加しました。

  • サジェスト結果のテスト(ぴかちう → ピカチュウ)
  • サジェスト結果のテスト(てらぱらす → テラパゴス)
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('テラパゴス');
  });
});

テスト実行

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

終わりに

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

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

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2