Reactコンポーネントのテストを書こう
このチュートリアルでは、Reactコンポーネントのテストを書くことを学びます。
本章で学べること
本章では、簡単なコンポーネントのテストを書くことを目標に、具体的には次のことをやっていきます。
- UIテストのためのライブラリ群
testing-libraryを使ったテストの作成 Jestを使ったスナップショットテストの作成
本章の目的はコンポーネントのテストを完全に理解することではありません。むしろ、それがどういったものなのか、その雰囲気を実際に体験することに主眼を置いています。そのため、内容はかなり最低限のものとなりますが、逆に言えば少しの時間でコンポーネントテストを試してみれるシンプルな内容にまとまってますから、ぜひ手を動かしてみてください。
Reactでコンポーネントが作れることを前提にしますので、Reactの基本的な使い方を知りたいという方はReactでいいねボタンを作ろうをご参照ください。
このチュートリアルで作成するテストコードの完成形はGitHubで確認することができます。
このチュートリアルに必要なもの
このチュートリアルをやるに当たって、必要なツールがあります。それらはここにリストアップしておくのであらかじめ用意しておいてください。
- Node.js (このチュートリアルではv18.15.0で動作確認しています)
- NPM
- Yarn v1系 (このチュートリアルはv1.22.19で動作確認しています)
Node.jsの導入については、開発環境の準備をご覧ください。
パッケージ管理ツールとしてYarnを利用します。最初にインストールをしておきましょう。すでにインストール済みの方はここのステップはスキップして大丈夫です。
shellnpm install -g yarn
shellnpm install -g yarn
Reactプロジェクトの作成
テストに使用するためのReactプロジェクトを作成します。下記コマンドを実行してください。
shellyarn create react-app component-test-tutorial --template typescript
shellyarn create react-app component-test-tutorial --template typescript
成功すると今いるディレクトリ配下にcomponent-test-tutorialというディレクトリが作られます。そのまま下記コマンドを実行してcomponent-test-tutorialに移動しましょう。
shellcd component-test-tutorial
shellcd 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
ここで次のコマンドを実行してください。
shellyarn start
shellyarn start
自動的にブラウザが開かれて次の画像のように表示されれば、プロジェクト作成が成功しています。

テストするコンポーネント
ここでは、簡単なボタンコンポーネントのテストを書くことを例に進めていきます。具体的には、はじめはOFFとなっているボタン上の文字が、ボタンをクリックするたびにON/OFFと切り替わるようなボタンを題材にします。

このコンポーネントについて、ボタンをクリックするとON/OFFの表示が切り替わることをテストしましょう。
テスト対象のコンポーネントを作る
テストを作成するために、まずはテスト対象となるコンポーネントを実装していきます。srcディレクトリ配下に、SimpleButton.tsxという名前でファイルを作成してください。
shellcd srctouch SimpleButton.tsx
shellcd 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.tsxtsximport {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.tsxtsximport {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.tsxtsximportReact 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.tsxtsximportReact 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 >);
そのうえで下記コマンドを実行しましょう。
shellyarn start
shellyarn start
すると、ブラウザが自動で立ち上がり、次のようなボタンが表示されます。初めはOFFと表示され、クリックによりONとOFFが交互に切り替わることを確認してください。

ボタンが小さければ、ブラウザの拡大率を上げてみると大きく表示されます。
これで今回テストするコンポーネントを作成できました。
testing-libraryを使ったテストの作り方とやり方
ここからはテストの作り方とやり方に入ります。今回は、ボタンをクリックするとON/OFFの表示が切り替わることをテストしていきます。
Reactコンポーネントをテストする方法は複数ありますが、ここでは利用者が比較的多いtesting-libraryというライブラリ群を用いる方法を紹介します。testing-libraryはUIコンポーネントのテストをするためのライブラリ群であり、コンポーネントの描画やコンポーネントに対する操作などが実現できます。testing-libraryがあれば、コンポーネントのテストはひととおりできると考えてよいでしょう。
testing-libraryをインストールする
次のコマンドを実行してtesting-libraryをインストールしてください。
shellyarn add \@testing-library/react@14 \@testing-library/jest-dom@5 \@testing-library/user-event@14
shellyarn add \@testing-library/react@14 \@testing-library/jest-dom@5 \@testing-library/user-event@14
テストを作る
それでは、実際にtesting-libraryを使ってテストを作っていきましょう。まずは先ほどと同じsrcディレクトリ配下でSimpleButton.test.tsxというファイルを作成します。
shelltouch SimpleButton.test.tsx
shelltouch SimpleButton.test.tsx
このファイルに、テストを実行するためのひな形を書きます。
SimpleButton.test.tsxtsxtest ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {// ここにテストの中身を書いていきます});
SimpleButton.test.tsxtsxtest ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {// ここにテストの中身を書いていきます});
ここにテストの中身を追加していきます。今回はボタンをクリックするとON/OFFの表示が切り替わることがテストしたいので、次のような流れのテストコードになります。
- ボタンを描画する
OFFと表示されていることを確かめる- ボタンをクリックする
ONと表示されていることを確かめる
コンポーネントのテストは、コンポーネントを描画した後、次の2つのことを組み合わせて実現されます。
- コンポーネントに操作を施す
- コンポーネントの状態を確かめる
今回の例もボタンを描画した後、「OFFと表示されている」という状態確認から始まり、「クリック」という操作を施した後、再び「ONと表示されている」という状態確認をします。みなさんが自分でコンポーネントのテストを書く際も、どのような操作と状態確認を行えばよいかを意識することでテスト作成がスムーズにできるはずです。
まずはボタンを描画してみましょう。コンポーネントの描画は@testing-library/reactのrender()を使って、次のようにするだけです。なお、この@testing-library/reactというライブラリは、今回yarn create react-appでReactアプリケーションを作成したためすでにプロジェクトにインストールされています。
SimpleButton.test.tsxtsximport {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);});
SimpleButton.test.tsxtsximport {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.tsxtsximport {render ,screen } from "@testing-library/react";// ^^^^^^追加import {SimpleButton } from "./SimpleButton";test ("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {render (<SimpleButton />);constsimpleButton =screen .getByRole ("button");});
SimpleButton.test.tsxtsximport {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.tsxtsximport {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.tsxtsximport {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コマンドでテストを実行し、テストが通ることを確認しましょう。
shellyarn test
shellyarn test
次のような結果になるはずです。

さて、次にボタンをクリックします。コンポーネントの操作はtesting-libraryに収録されている@testing-library/user-eventを使って実現できます。@testing-library/user-eventはコンポーネントの操作を含む、色々なユーザーイベントをテストで実行するライブラリです。具体的にはclick()にクエリでみつけたsimpleButtonを引数として渡すことで、ボタンのクリックを実現できます。
SimpleButton.test.tsxtsximport {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.tsxtsximport {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.tsxtsximport {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.tsxtsximport {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というファイルを作成します。
shelltouch SimpleButton.test.tsx
shelltouch SimpleButton.test.tsx
「testing-libraryを使ったテストの作り方とやり方」から続けてこのチュートリアルを実施される方は、ここから作成するテストケースをSimpleButton.test.tsx内に追加で書いていくのでも大丈夫です。
スナップショットテストは次の2ステップから成ります。
- スナップショットを検証したい状態にコンポーネントを持っていく
- スナップショットに照合する
ここではボタンが描画されてまだ何も操作されていない状態、つまりボタンにOFFと表示されている状態についてスナップショットテストを実施することを考えます。描画されたばかりの状態を検証したいので、描画してすぐにスナップショット照合を行えばよいことになります。
この考えをもとに、実際のコードを書いてみましょう。コンポーネントの描画には@testing-library/reactのrender関数を、スナップショットの照合にはJestのtoMatchSnapshot()関数をそれぞれ使用して次のように書くことができます。
SimpleButton.test.tsxtsximport {render } from "@testing-library/react";import {SimpleButton } from "./SimpleButton";test ("描画されてすぐはOFFと表示されている", () => {constview =render (<SimpleButton />);expect (view .container ).toMatchSnapshot ();});
SimpleButton.test.tsxtsximport {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コマンドを実行します。
shellyarn test
shellyarn 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.tsxtsximport {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.tsxtsximport {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コマンドを実行します。
shellyarn test
shellyarn test
先ほどのスナップショットテストが実行されますが、今回はテストが通らず、描画されたコンポーネントとスナップショットの差分が表示されます。

今回はボタン内テキストの初期値を変更しましたが、たとえばbuttonタグからdivタグへの変更やbuttonタグへのクラスの追加など、DOMに対する変更のほとんどをスナップショットテストで検知できます。
スナップショットテストの詳しいやり方やベストプラクティスなど、さらに詳しい情報に触れたい方はJestの公式ドキュメントをご参照ください。
以上でJestを使ったスナップショットテストのチュートリアルは完了です。また、Reactコンポーネントのテストのチュートリアルも完了です。