Reactコンポーネントのテストを書こう
このチュートリアルでは、Reactコンポーネントのテストを書くことを学びます。
本章で学べること
本章では、簡単なコンポーネントのテストを書くことを目標に、具体的には次のことをやっていきます。
- UIテストのためのライブラリ群
testing-library
を使ったテストの作成 Jest
を使ったスナップショットテストの作成
本章の目的はコンポーネントのテストを完全に理解することではありません。むしろ、それがどういったものなのか、その雰囲気を実際に体験することに主眼を置いています。そのため、内容はかなり最低限のものとなりますが、逆に言えば少しの時間でコンポーネントテストを試してみれるシンプルな内容にまとまってますから、ぜひ手を動かしてみてください。
Reactでコンポーネントが作れることを前提にしますので、Reactの基本的な使い方を知りたいという方はReactでいいねボタンを作ろうをご参照ください。
このチュートリアルで作成するテストコードの完成形はGitHubで確認することができます。
このチュートリアルに必要なもの
このチュートリアルをやるに当たって、必要なツールがあります。それらはここにリストアップしておくのであらかじめ用意しておいてください。
- Node.js (このチュートリアルではv18.15.0で動作確認しています)
- NPM
- Yarn v1系 (このチュートリアルはv1.22.19で動作確認しています)
Node.jsの導入については、開発環境の準備をご覧ください。
パッケージ管理ツールとしてYarnを利用します。最初にインストールをしておきましょう。すでにインストール済みの方はここのステップはスキップして大丈夫です。
shell
npm install -g yarn
shell
npm install -g yarn
Reactプロジェクトの作成
テストに使用するためのReactプロジェクトを作成します。下記コマンドを実行してください。
shell
yarn create react-app component-test-tutorial --template typescript
shell
yarn create react-app component-test-tutorial --template typescript
成功すると今いるディレクトリ配下にcomponent-test-tutorial
というディレクトリが作られます。そのまま下記コマンドを実行してcomponent-test-tutorial
に移動しましょう。
shell
cd component-test-tutorial
shell
cd component-test-tutorial
component-test-tutorial
配下のファイル構成は次のようになっているはずです。
text
├── .gitignore├── node_modules├── README.md├── package.json├── public│ ├── favicon.ico│ ├── index.html│ ├── logo192.png│ ├── logo512.png│ ├── manifest.json│ └── robots.txt├── src│ ├── App.css│ ├── App.test.tsx│ ├── App.tsx│ ├── index.css│ ├── index.tsx│ ├── logo.svg│ ├── react-app-env.d.ts│ ├── reportWebVitals.ts│ └── setupTests.ts├── tsconfig.json└── yarn.lock
text
├── .gitignore├── node_modules├── README.md├── package.json├── public│ ├── favicon.ico│ ├── index.html│ ├── logo192.png│ ├── logo512.png│ ├── manifest.json│ └── robots.txt├── src│ ├── App.css│ ├── App.test.tsx│ ├── App.tsx│ ├── index.css│ ├── index.tsx│ ├── logo.svg│ ├── react-app-env.d.ts│ ├── reportWebVitals.ts│ └── setupTests.ts├── tsconfig.json└── yarn.lock
ここで次のコマンドを実行してください。
shell
yarn start
shell
yarn start
自動的にブラウザが開かれて次の画像のように表示されれば、プロジェクト作成が成功しています。
テストするコンポーネント
ここでは、簡単なボタンコンポーネントのテストを書くことを例に進めていきます。具体的には、はじめはOFF
となっているボタン上の文字が、ボタンをクリックするたびにON
/OFF
と切り替わるようなボタンを題材にします。
このコンポーネントについて、ボタンをクリックするとON
/OFF
の表示が切り替わることをテストしましょう。
テスト対象のコンポーネントを作る
テストを作成するために、まずはテスト対象となるコンポーネントを実装していきます。src
ディレクトリ配下に、SimpleButton.tsx
という名前でファイルを作成してください。
shell
cd srctouch SimpleButton.tsx
shell
cd srctouch SimpleButton.tsx
このファイルを作ると、src
ディレクトリのファイル構成は次のようになります。
text
├── App.css├── App.test.tsx├── App.tsx├── SimpleButton.tsx├── index.css├── index.tsx├── logo.svg├── react-app-env.d.ts├── reportWebVitals.ts└── setupTests.ts
text
├── App.css├── App.test.tsx├── App.tsx├── SimpleButton.tsx├── index.css├── index.tsx├── logo.svg├── react-app-env.d.ts├── reportWebVitals.ts└── setupTests.ts
SimpleButton.tsx
の内容は次のようにします。
SimpleButton.tsxtsx
import {useState } from "react";export constSimpleButton : () =>JSX .Element = () => {const [state ,setState ] =useState (false);consthandleClick = () => {setState ((prevState ) => !prevState );};return <button onClick ={handleClick }>{state ? "ON" : "OFF"}</button >;};
SimpleButton.tsxtsx
import {useState } from "react";export constSimpleButton : () =>JSX .Element = () => {const [state ,setState ] =useState (false);consthandleClick = () => {setState ((prevState ) => !prevState );};return <button onClick ={handleClick }>{state ? "ON" : "OFF"}</button >;};
ここで、このSimpleButton
コンポーネントの挙動を確認してみましょう。index.tsx
ファイルを次のようにして保存してください。
index.tsxtsx
importReact from "react";importReactDOM from "react-dom/client";import {SimpleButton } from "./SimpleButton";constroot =ReactDOM .createRoot (document .getElementById ("root") asHTMLElement );root .render (<React .StrictMode ><SimpleButton /></React .StrictMode >);
index.tsxtsx
importReact from "react";importReactDOM from "react-dom/client";import {SimpleButton } from "./SimpleButton";constroot =ReactDOM .createRoot (document .getElementById ("root") asHTMLElement );root .render (<React .StrictMode ><SimpleButton /></React .StrictMode >);
そのうえで下記コマンドを実行しましょう。
shell
yarn start
shell
yarn start
すると、ブラウザが自動で立ち上がり、次のようなボタンが表示されます。初めはOFF
と表示され、クリックによりON
とOFF
が交互に切り替わることを確認してください。
ボタンが小さければ、ブラウザの拡大率を上げてみると大きく表示されます。
これで今回テストするコンポーネントを作成できました。
testing-library
を使ったテストの作り方とやり方
ここからはテストの作り方とやり方に入ります。今回は、ボタンをクリックするとON
/OFF
の表示が切り替わることをテストしていきます。
Reactコンポーネントをテストする方法は複数ありますが、ここでは利用者が比較的多いtesting-library
というライブラリ群を用いる方法を紹介します。testing-library
はUIコンポーネントのテストをするためのライブラリ群であり、コンポーネントの描画やコンポーネントに対する操作などが実現できます。testing-library
があれば、コンポーネントのテストはひととおりできると考えてよいでしょう。
testing-libraryをインストールする
次のコマンドを実行してtesting-libraryをインストールしてください。
shell
yarn add \@testing-library/react@14 \@testing-library/jest-dom@5 \@testing-library/user-event@14
shell
yarn add \@testing-library/react@14 \@testing-library/jest-dom@5 \@testing-library/user-event@14
テストを作る
それでは、実際にtesting-library
を使ってテストを作っていきましょう。まずは先ほどと同じsrc
ディレクトリ配下でSimpleButton.test.tsx
というファイルを作成します。
shell
touch SimpleButton.test.tsx
shell
touch SimpleButton.test.tsx
このファイルに、テストを実行するためのひな形を書きます。
SimpleButton.test.tsxtsx
test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {// ここにテストの中身を書いていきます});
SimpleButton.test.tsxtsx
test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {// ここにテストの中身を書いていきます});
ここにテストの中身を追加していきます。今回はボタンをクリックするとON
/OFF
の表示が切り替わることがテストしたいので、次のような流れのテストコードになります。
- ボタンを描画する
OFF
と表示されていることを確かめる- ボタンをクリックする
ON
と表示されていることを確かめる
コンポーネントのテストは、コンポーネントを描画した後、次の2つのことを組み合わせて実現されます。
- コンポーネントに操作を施す
- コンポーネントの状態を確かめる
今回の例もボタンを描画した後、「OFF
と表示されている」という状態確認から始まり、「クリック」という操作を施した後、再び「ON
と表示されている」という状態確認をします。みなさんが自分でコンポーネントのテストを書く際も、どのような操作と状態確認を行えばよいかを意識することでテスト作成がスムーズにできるはずです。
まずはボタンを描画してみましょう。コンポーネントの描画は@testing-library/react
のrender()
を使って、次のようにするだけです。なお、この@testing-library/react
というライブラリは、今回create-react-app
でReactアプリケーションを作成したためすでにプロジェクトにインストールされています。
SimpleButton.test.tsxtsx
import {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);});
SimpleButton.test.tsxtsx
import {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);});
ボタンが描画されたので、次はOFF
と表示されていることを確かめます。具体的には、ボタンのDOM(DOMとは、ここではボタンを表すオブジェクトくらいに捉えていただければ大丈夫です)を取得し、そのテキストがOFF
という文字列に等しいかのアサーションを実施します。今回、ボタンのDOMの取得には@testing-library/react
が提供するクエリのひとつであるgetByRole()
を使います。これはWAI-ARIA(アクセシビリティ向上を主目的として定められたwebの仕様)で定められたRoleを引数に指定すると、そのRoleを持つコンポーネントを取得するクエリです。詳細は公式ドキュメントをご参照ください。具体的には、このように書けます。
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";// ^^^^^^追加import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");});
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";// ^^^^^^追加import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");});
そして、ボタンのテキストのアサーションは@testing-library/jest-dom
が提供するtoHaveTextContent()
を使います。expect()
にコンポーネントを渡し、そのままtoHaveTextContent()
を呼び出すと、そのコンポーネントがどのようなテキストを持っているかのアサーションが行なえます。具体的には次のようになります。
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");});
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");});
ここで一旦yarn test
コマンドでテストを実行し、テストが通ることを確認しましょう。
shell
yarn test
shell
yarn test
次のような結果になるはずです。
さて、次にボタンをクリックします。コンポーネントの操作はtesting-library
に収録されている@testing-library/user-event
を使って実現できます。@testing-library/user-event
はコンポーネントの操作を含む、色々なユーザーイベントをテストで実行するライブラリです。具体的にはclick()
にクエリでみつけたsimpleButton
を引数として渡すことで、ボタンのクリックを実現できます。
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";importuserEvent from "@testing-library/user-event";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {constuser =userEvent .setup ();render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");awaituser .click (simpleButton );});
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";importuserEvent from "@testing-library/user-event";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {constuser =userEvent .setup ();render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");awaituser .click (simpleButton );});
続けて、ボタンがクリックされた後のアサーションを実施します。先ほどと同様にtoHaveTextContent()
を用いますが、今度はボタンのテキストがON
になっていることを確認しましょう。
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";importuserEvent from "@testing-library/user-event";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {constuser =userEvent .setup ();render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");awaituser .click (simpleButton );expect (simpleButton ).toHaveTextContent ("ON");});
SimpleButton.test.tsxtsx
import {render ,screen } from "@testing-library/react";importuserEvent from "@testing-library/user-event";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {constuser =userEvent .setup ();render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");expect (simpleButton ).toHaveTextContent ("OFF");awaituser .click (simpleButton );expect (simpleButton ).toHaveTextContent ("ON");});
この状態でyarn test
コマンドでテストを実行し、テストが通ることを確認しましょう。次のような結果になるはずです。
以上が、testing-library
を用いてコンポーネントのテストを作成する流れです。testing-library
からは、ここで紹介したもの以外にも多くのクエリやアサーション、ユーザーイベントの機能が提供されています。英語にはなってしまいますが、クエリはこちら、アサーションはこちら、ユーザーイベントはこちらに公式ドキュメントによる詳細な説明があります。実際に自分でテストを作る際には、ぜひそれらも確認してみてください。
Jest
を使ったスナップショットテストの作り方とやり方
ここからは「スナップショットテスト」と呼ばれるテスト手法について解説します。
先ほどまでのテストはコンポーネントのある部分(例: テキスト)の状態を確認するものでしたが、「スナップショットテスト」はコンポーネントの全体の状態を確かめるためのテストです。より正確には、コンポーネントのDOMをまるごと保存し、その保存したDOMと、テスト実行時にコンポーネントを描画して生成したDOMとが一致するかを確認します(DOMとは何かがよく分からない場合、ここではひとまず「コンポーネントを表すオブジェクト」程度に捉えてください)。
「スナップショットテスト」は簡単に書くことができます。それでいてスタイルなど含めた全体の確認ができるので、手軽なリグレッションテストとして活用できます。一方で、そうであるからこそコンポーネントを一旦作り終えるまでは機能しないテストですので、テストファーストの開発には不向きです。
本来、スナップショットテストの対象はコンポーネントおよびDOMに限られたものではありません。幅広い対象にスナップショットテストが実施できます。詳しくはJestの公式ドキュメントをご参照ください。
それでは、スナップショットテストを実際にやってみましょう。先ほどと同じsrc
ディレクトリ配下でSimpleButton.test.tsx
というファイルを作成します。
shell
touch SimpleButton.test.tsx
shell
touch SimpleButton.test.tsx
「testing-library
を使ったテストの作り方とやり方」から続けてこのチュートリアルを実施される方は、ここから作成するテストケースをSimpleButton.test.tsx
内に追加で書いていくのでも大丈夫です。
スナップショットテストは次の2ステップから成ります。
- スナップショットを検証したい状態にコンポーネントを持っていく
- スナップショットに照合する
ここではボタンが描画されてまだ何も操作されていない状態、つまりボタンにOFFと表示されている状態についてスナップショットテストを実施することを考えます。描画されたばかりの状態を検証したいので、描画してすぐにスナップショット照合を行えばよいことになります。
この考えをもとに、実際のコードを書いてみましょう。コンポーネントの描画には@testing-library/react
のrender
関数を、スナップショットの照合にはJestのtoMatchSnapshot()
関数をそれぞれ使用して次のように書くことができます。
SimpleButton.test.tsxtsx
import {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("描画されてすぐはOFFと表示されている", () => {constview =render (<SimpleButton />);expect (view .container ).toMatchSnapshot ();});
SimpleButton.test.tsxtsx
import {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("描画されてすぐはOFFと表示されている", () => {constview =render (<SimpleButton />);expect (view .container ).toMatchSnapshot ();});
Jest単体ではReactコンポーネントの描画ができません。そこで、コンポーネントの描画をするためのライブラリを使用する必要があります。多くのライブラリがありますが、ここでは前章「testing-library
を使ったテストの作り方とやり方」でも紹介した@testing-library/react
を用いました。
テストファイルが作成できたら、yarn test
コマンドを実行します。
shell
yarn test
shell
yarn test
そうすると次のように表示され、テストが実行されて成功した(PASS
した)ことがわかります。
さて、このときsrc
ディレクトリの中に__snapshots__
というディレクトリが自動で追加されているはずです。これはJestがスナップショットテスト用のファイルを保存していくためのフォルダです。Jestのスナップショットテストは初回実行時にスナップショットテスト用のファイルを生成し、2回目から照合を行います。いまは初回実行だったため、ファイルとその置き場であるディレクトリが自動で生成されました。
ここでスナップショットテストについてもう少しだけ知るために、生成されたスナップショットテスト用のファイルの中身を覗いてみましょう。
__snapshots__
ディレクトリの中に作られたSimpleButton.test.tsx.snap
は次のようになっています。
SimpleButton.test.tsx.snapjs
// Jest Snapshot v1, https://goo.gl/fbAQLPexports [`描画されてすぐはOFFと表示されている 1`] = `<div><button>OFF</button></div>`;
SimpleButton.test.tsx.snapjs
// Jest Snapshot v1, https://goo.gl/fbAQLPexports [`描画されてすぐはOFFと表示されている 1`] = `<div><button>OFF</button></div>`;
このように、スナップショットテスト用のファイルはテストケースの名前と、そのテストケースで使われるスナップショットで構成されています。
さて、今回生成されたスナップショットはOFF
というテキストを持ったbutton
タグと、その親要素であるdiv
タグで構成されています。これは、まさに先ほど作ったSimpleButton
コンポーネントのDOMに一致します(div
要素はReactの起動時に自動生成される要素です)。このスナップショットテストは実行のたびに、SimpleButton
コンポーネントを描画して、たった今作られたこのスナップショットとの違いが生まれていないかを確認してくれます。
たとえば、もしも何かの手違いでSimpleButton
コンポーネントが描画されたときにON
と表示されるようになっていたら、このスナップショットテストに引っかかるのです。
ここで、実際に失敗する様子も確認してみましょう。SimpleButton
コンポーネントが描画されたときにON
と表示されるよう変更を加えます。
SimpleButton.tsxtsx
import {useState } from "react";export constSimpleButton : () =>JSX .Element = () => {const [state ,setState ] =useState (true);// falseからtrueに変更 ^^^^consthandleClick = () => {setState ((prevState ) => !prevState );};return <button onClick ={handleClick }>{state ? "ON" : "OFF"}</button >;};
SimpleButton.tsxtsx
import {useState } from "react";export constSimpleButton : () =>JSX .Element = () => {const [state ,setState ] =useState (true);// falseからtrueに変更 ^^^^consthandleClick = () => {setState ((prevState ) => !prevState );};return <button onClick ={handleClick }>{state ? "ON" : "OFF"}</button >;};
この状態でyarn start
コマンドを実行すると、描画されたボタンの文字の初期値がON
になっていることが分かります。
さて、ここでyarn test
コマンドを実行します。
shell
yarn test
shell
yarn test
先ほどのスナップショットテストが実行されますが、今回はテストが通らず、描画されたコンポーネントとスナップショットの差分が表示されます。
今回はボタン内テキストの初期値を変更しましたが、たとえばbutton
タグからdiv
タグへの変更やbutton
タグへのクラスの追加など、DOMに対する変更のほとんどをスナップショットテストで検知できます。
スナップショットテストの詳しいやり方やベストプラクティスなど、さらに詳しい情報に触れたい方はJestの公式ドキュメントをご参照ください。
以上でJestを使ったスナップショットテストのチュートリアルは完了です。また、Reactコンポーネントのテストのチュートリアルも完了です。