はじめに
Reactで個人的に色々作ったり勉強することが多く、「テストまで回す癖つけた方がいいよ!」というアドバイスをいただいたので、初めてのReactでのテストの備忘録として書いてます。なので初歩的な内容です。。。
Jestとは
それぞれのプログラミング言語には、テストを補助するためのフレームワークが存在します。
Jestは、JavaScriptのテストフレームワークです。
Babel、TypeScript、Node、React、Angular、Vue などの様々なフレームワークを利用したプロジェクトで動作します!(公式引用)
Jest公式:https://jestjs.io/ja/
とりあえず、テストファイルを見てみる
① Reactのプロジェクトを作る
npx create-react-app my-app --template typescript
プロジェクトを作ると、自動的に
App.test.tsx
setupTests.ts
というファイルが作成されます。
② App.test.tsxを見てみる
App.test.tsxには既に、下記のようなコードが書かれています。
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
上記では、Appコンポーネント
のテストを行っています。
③ setupTests.tsを見てみる
テストの初期化設定をするファイルです。
create react app
のサイトにsetupTests.jsについて書いてあります。
この機能は、react-scripts@0.4.0 以降で利用可能です。
アプリがテストでモックする必要のあるブラウザAPIを使用している場合、またはテストを実行する前にグローバルな設定が必要な場合は、プロジェクトに src/setupTests.js を追加してください。これは、テストを実行する前に自動的に実行されます。
setupTests.tsファイルには、下記のように書かれています。
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
※ アサーション
とは、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組み。(←自分がわからなかったので、自分用に記述)
④ テストを走らせてみる
npm test
すると、下記のように表示されるので、とりあえずaキー
を押して全てのテストを実行します。
No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.
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.
ーーー
// 実行結果
PASS src/App.test.tsx
✓ renders learn react link (44 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.308 s
Ran all test suites.
Watch Usage: Press w to show more.
元々のソースを何もいじってないので通ります。
wキー
を押し、qキー
を押してウォッチモードを終了します。
Jestについて理解する
① テストの書き方
test('テストケースの名前', () => {
// テスト
});
② 関連テストをまとめる
describe()
を使うことで、いくつかの関連するテストをまとめたブロックを作成することができます。
describe('ブロック名', () => {
test('テストケース1', () => {
// テスト
});
test('テストケース2', () => {
// テスト
});
});
参考:https://jestjs.io/ja/docs/api#describename-fn
③ テストの条件を定義する
Matcher(マッチャー)
を使って、テストの評価条件を定義します。
Matcherはメソッドです。テスト実行時に、どんな結果が返ってきてほしいかを定義して、失敗成功を判定するようです。
下記のように書きます。
expect(値).マッチャー();
expect(値)
は、"expectation" オブジェクトを返します。
expect関数には、テストしたい関数やコンポーネントを渡してあげます。
// テストしたい関数
function calcFactorial(num: number):number {
let result: number = 1;
for(let i = num; i > 0;i--) {
result = result * i;
}
return result;
}
// テスト
test('/utils/calc.tsのcalcFactorial関数のテスト', () => {
const num: number = 4;
console.log(calcFactorial(num));
expect(calcFactorial(num)).toStrictEqual(24);
});
Matcherの一覧やそれぞれのメソッドの使い方をわかりやすくまとめてくれているサイトがたくさんありますので、そちらを参考にしてみてください。
テスト書いてみる
フォルダ構成は以下のようにしました。
src
L App.css
L App.test.tsx
L App.tsx
L index.css
L index.tsx
L logo.svg
L react-app-env.d.ts
L reportWebVitals.ts
L setupTests.ts
L __tests__ <<- 追加
L list.test.tsx <<- 追加
L components <<- 追加
L list <<- 追加
L list.module.css <<- 追加
L list.tsx <<- 追加
L utils <<- 追加
L calc.ts <<- 追加
① TypeScriptの簡易的なテスト
まずは、「TypeScriptで書いた関数が意図した結果を返してくれるかどうか」くらいのテストをやってみます。
まずテスト用の関数の作成
配列を受け取り、配列中に2つ以上発生する数値を除去した配列を返す関数を作ります。(語彙力が乏しくてすみません)
function removeDuplicates(arr: number[]):number[] {
let result:number[] = [];
arr.forEach((num, i) => {
let count = 0;
arr.forEach((num2) => {
if(num === num2) count ++;
});
// 配列中に同じ値が1つだけ出現したやつをresult配列に
if(count === 1) result.push(num);
});
return result;
}
module.exports = removeDuplicates;
テストを書く
とりあえず、App.test.tsx
に記述していきます。
test('/utils/calc.tsのremoveDuplicates関数のテスト', () => {
const numArr: number[] = [3, 4, 2, 6, 2, 2, 1, 3, 4, 5, 7];
const removeDuplicates = require('./utils/calc');
console.log(removeDuplicates(numArr));
expect(removeDuplicates(numArr)).toStrictEqual([6, 1, 5, 7]);
});
Reactでテストって何から書けばいいのか正直わからなかったのですが、
「console.log
などで変数の中身を表示する時ってあるじゃない?
エラー出たら出力する為に書いて、解決して消して、またエラー出たから書いて、、、ってやっていると面倒だから、console.logをテストに書いておいて、テスト回して出力確認するっていうのから初めてみたらいいのよん!」
とアドバイスをいただき、個人学習でテストを行うハードルがかなり下がりました
結果はこんな感じ↓ wキー
押して、qキー
押してウォッチモードを終了します。
console.log
[ 6, 1, 5, 7 ]
at Object.<anonymous> (src/App.test.tsx:16:11)
PASS src/App.test.tsx
✓ renders learn react link (47 ms)
✓ /utils/calc.tsのremoveDuplicates関数のテスト (53 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.627 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
引っ掛かったらこんな感じ
テスト時に使うMatcher間違えた時のエラーです!
通ったテストと、通らなかったテストがわかりやすいですね!
FAIL src/App.test.tsx
✓ renders learn react link (46 ms)
✕ /utils/calc.tsのremoveDuplicates関数のテスト (27 ms)
● /utils/calc.tsのremoveDuplicates関数のテスト
expect(received).toBe(expected) // Object.is equality
If it should pass with deep equality, replace "toBe" with "toStrictEqual"
Expected: [6, 1, 5, 7]
Received: serializes to the same string
14 | const removeDuplicates = require('./utils/calc');
15 |
> 16 | expect(removeDuplicates(numArr)).toBe([6, 1, 5, 7]);
| ^
17 | });
at Object.<anonymous> (src/App.test.tsx:16:36)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.594 s
Ran all test suites related to changed files.
② コンポーネントのテスト
「コンポーネントが意図したように表示されるかどうか」のテストをやってみます。
コンポーネントのテスト観点
下記のサイトがすごく参考になりました。
まずテスト用のコンポーネントの作成
propsでitems
、headerData
を受け取り、リストを表示するコンポーネントです。
headerData
が渡された時は、リストにヘッダーを表示するようにしています。
import React from "react";
/* import css */
import styles from './list.module.css';
type Props = {
items: {title: string, description?: string, icon?: React.ReactNode}[],
headerData?: string
}
export default function ListWithIcon(props: Props){
const {items, headerData} = props;
return(
<ul className={styles.listWrapper}>
{ headerData && <li className={styles.header}> { headerData } </li> }
{
items.length === 0 ?
<span>表示するデータがありません。</span>
:
items.map((item, index) => {
return(
<li key={index}>
<span className={styles.leftContent}>
{item.icon &&
<span className={styles.iconWrapper}>
{ item.icon }
</span>
}
<span className={styles.title}>{item.title}</span>
</span>
<span>{item.description}</span>
</li>
)
})
}
</ul>
)
}
.listWrapper {
padding: 0;
}
.listWrapper li {
padding: 0.5rem 1rem;
border-radius: var(--my-border-radius-s);
background-color: var(--my-color-bgColor);
list-style: none;
margin-bottom: 0.5rem;
color: var(--my-color-navy);
transition: all 0.5s;
display: flex;
align-items: center;
justify-content: space-between;
}
.listWrapper li:not(.header):hover {
opacity: 0.5;
transition: all 0.5s;
}
.listWrapper .header {
background-color: var(--my-color-navy);
color: var(--my-color-white);
border-radius: var(--my-border-radius-s) var(--my-border-radius-s) 0 0;
}
.leftContent {
display: flex;
align-items: center;
justify-content: left;
}
.title {
font-weight: bold;
margin: auto var(--my-margin-s);
}
.iconWrapper {
background-color: var(--my-color-green);
border-radius: var(--my-border-radius-circle);
color: var(--my-color-white);
min-width: var(--my-font-size-h1);
min-height: var(--my-font-size-h1);
padding: var(--my-padding-xs);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--my-font-size-p);
}
テストを書いてみる
新しく__tests__
の中にlist.test.tsx
ファイルを追加し、テストを記述します!
今回テストしたい内容は下記になります。
「headerDataを渡した場合はリストにヘッダーを表示し、渡さない場合は非表示になるかどうか
」
なので、
headerDataを渡してコンポーネントを描画し、ヘッダーが表示されるか確認するパターン と
headerDataを渡さずにコンポーネントを描画し、ヘッダーが非表示になるか確認するパターン の
2つのテストを記述します!!
import { render, screen } from '@testing-library/react'
import ListWithIcon from '../component/list/list';
import CategoryIcon from '@mui/icons-material/Category';
describe('components/listテスト', () => {
// ListWithIconコンポーネントに渡すデータ
const listItems: {title: string, description?: string, icon?: React.ReactNode}[] = [
{title: 'タイトル1'},
{title: 'タイトル2', description: '説明がきです〜'},
{title: 'タイトル3', description: 'ディスクリプションです〜', icon: <CategoryIcon fontSize="small" />}
]
/* -- テスト1 -- */
test('headerDataを渡しヘッダーを表示する', () => {
// render()関数を使ってコンポーネントを描画(headerDataを渡す)
const view = render(<ListWithIcon items={listItems} headerData='リストのヘッダーだよ'/>);
// 興味本位でconsole.log
console.log('view: ', view);
// 要素の取得(@testing-library/reactのクエリ ` queryByTestId ` を使用)
const listDom = screen.queryByTestId('list-header');
// ちゃんと返ってくる
console.log(listDom);
// nullにならないことを期待する
expect(listDom).not.toBeNull();
})
/* -- テスト2 -- */
test('headerDataを渡さずヘッダーを表示しない', () => {
// render()関数を使ってコンポーネントを描画(headerDataを渡さない)
render(<ListWithIcon items={listItems} />);
// 要素の取得(@testing-library/reactのクエリ ` queryByTestId ` を使用)
const listDom = screen.queryByTestId('list-header');
// nullが返ってくる
console.log(listDom);
// nullになることを期待する
expect(listDom).toBeNull();
})
})
特定のDOMを取得には、getByRole()
メソッドを使用するのですが、今回自分のコンポーネント定義の仕方が悪くて、リストのヘッダーもそれ以外もliタグで描いちゃってるので、getByRole('listitem')で指定しちゃうと、TestingLibraryElementErrorです: 役割 "listitem" を持つ要素が複数見つかりました。
と怒られちゃいました。
複数要素を探す方法もあるのですが、今回はヘッダーにあたるliの表示非表示をテストしたいということで、色々調べて、あまり推奨されてはいませんが、とりあえず挙動を確認したいのでgetByTestId()
を使うことにしました。
テストで利用したい要素にdata-testid属性に設定した任意の名前でDOMを取得することができます。
Testing Libraryのクエリについて
sceren.クエリ();
ページ上の要素を見つけるためにTesting Libraryが提供するメソッドです。クエリにはいくつかの種類(「get」、「find」、「query」)があり、それらの違いは、要素が見つからなかった場合にエラーを投げるか、Promiseを返して再試行するかです。どのようなページのコンテンツを選択するかによって、異なるクエリがより適切であったり、より適切でなかったりすることがあります。最もアクセスしやすい方法でページをテストするためにセマンティッククエリを利用する方法に関する推奨事項については、優先順位ガイドを参照してください。
const 任意の名前 = render(コンポーネント); とすると、下記のようなエラーが出ました。
戻り値の名前は、view
またはutils
が推奨されているようです。
任意の名前
is not a recommended name forrender
returned value. Instead, you should destructure it, or name it using one of:view
, orutils
getByTestId()について
Testing Library
のサイトには、以下のように記載がありました。
In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure or styling css class names. Learn more about data-testids from the blog post "Making your UI tests resilient to change"
指導原則の精神に則り、他のクエリがあなたのユースケースで機能しない場合にのみ、これを使用することが推奨されます。data-testid属性の使用は、あなたのソフトウェアがどのように使用されているかに似ていないので、可能であれば避けるべきです。とはいえ、DOM構造やスタイリングCSSのクラス名に基づいてクエリを実行するよりは、はるかに優れています。data-testidについては、ブログポスト「Making your UI tests resilient to change」で詳しく解説しています。
最後に
次回は、setupTest.ts
の記述方法やイベントを使ったテスト、結合テストあたりを記事にできるくらい理解してこようと思います。
参考資料