概要
今回は、前回まで作って来たReactコンポーネントをテストするコード作成します。
(いままでの内容が気になる方は、ぜひご覧ください)
- 1:ChatGPTに3回質問して「文字の類似度」のサジェストするプログラム作成
- 2:「文字の類似度」からポケモン名をサジェストするコンポーネントを実装【React】
- 3:【React】JavaScriptからTypeScriptに移行してみた
カバレッジとは
カバレッジとはコード網羅率のことで、プログラムがテストされた割合の指標です。
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個表示されているかテストします。
テストコード実装
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%
と網羅度は低いのでテストを追加したほうがいいようです。
テスト追加②
テスト内容
文字を入力し、表示されたタグをクリックし、インプットタグを更新されるかテストします。
実際に操作すると以下のようになります。
テストコード実装
テストケースひとつだけだと心もとないので、以下の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%になったので、すべての処理をテストすることができました。(気持ちいいですよね)
終わりに
今回は、Reactのユニットテストを行うことができました。
こんなに簡単にWEB画面のテストができてしまうのは、すごいと思います。(実はWEB画面のテストは、初めてでした
ここまで読んでいただきありがとうございました。