メインコンテンツまでスキップ

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と切り替わる様子

このコンポーネントについて、ボタンをクリックするとON/OFFの表示が切り替わることをテストしましょう。

テスト対象のコンポーネントを作る

テストを作成するために、まずはテスト対象となるコンポーネントを実装していきます。srcディレクトリ配下に、SimpleButton.tsxという名前でファイルを作成してください。

shell
cd src
touch SimpleButton.tsx
shell
cd src
touch 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.tsx
tsx
import { useState } from "react";
 
export const SimpleButton: () => JSX.Element = () => {
const [state, setState] = useState(false);
const handleClick = () => {
setState((prevState) => !prevState);
};
return <button onClick={handleClick}>{state ? "ON" : "OFF"}</button>;
};
SimpleButton.tsx
tsx
import { useState } from "react";
 
export const SimpleButton: () => JSX.Element = () => {
const [state, setState] = useState(false);
const handleClick = () => {
setState((prevState) => !prevState);
};
return <button onClick={handleClick}>{state ? "ON" : "OFF"}</button>;
};

ここで、このSimpleButtonコンポーネントの挙動を確認してみましょう。index.tsxファイルを次のようにして保存してください。

index.tsx
tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { SimpleButton } from "./SimpleButton";
 
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<SimpleButton />
</React.StrictMode>
);
index.tsx
tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { SimpleButton } from "./SimpleButton";
 
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<SimpleButton />
</React.StrictMode>
);

そのうえで下記コマンドを実行しましょう。

shell
yarn start
shell
yarn start

すると、ブラウザが自動で立ち上がり、次のようなボタンが表示されます。初めはOFFと表示され、クリックによりONOFFが交互に切り替わることを確認してください。

ボタン上の文字がクリックによって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.tsx
tsx
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
// ここにテストの中身を書いていきます
});
SimpleButton.test.tsx
tsx
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
// ここにテストの中身を書いていきます
});

ここにテストの中身を追加していきます。今回はボタンをクリックするとON/OFFの表示が切り替わることがテストしたいので、次のような流れのテストコードになります。

  1. ボタンを描画する
  2. OFFと表示されていることを確かめる
  3. ボタンをクリックする
  4. ONと表示されていることを確かめる
備考

コンポーネントのテストは、コンポーネントを描画した後、次の2つのことを組み合わせて実現されます。

  1. コンポーネントに操作を施す
  2. コンポーネントの状態を確かめる

今回の例もボタンを描画した後、「OFFと表示されている」という状態確認から始まり、「クリック」という操作を施した後、再び「ONと表示されている」という状態確認をします。みなさんが自分でコンポーネントのテストを書く際も、どのような操作と状態確認を行えばよいかを意識することでテスト作成がスムーズにできるはずです。

まずはボタンを描画してみましょう。コンポーネントの描画は@testing-library/reactrender()を使って、次のようにするだけです。なお、この@testing-library/reactというライブラリは、今回yarn create react-appでReactアプリケーションを作成したためすでにプロジェクトにインストールされています。

SimpleButton.test.tsx
tsx
import { render } from "@testing-library/react";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
render(<SimpleButton />);
});
SimpleButton.test.tsx
tsx
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.tsx
tsx
import { render, screen } from "@testing-library/react";
// ^^^^^^追加
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
});
SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
// ^^^^^^追加
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
});

そして、ボタンのテキストのアサーションは@testing-library/jest-domが提供するtoHaveTextContent()を使います。expect()にコンポーネントを渡し、そのままtoHaveTextContent()を呼び出すと、そのコンポーネントがどのようなテキストを持っているかのアサーションが行なえます。具体的には次のようになります。

SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
});
SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
});

ここで一旦yarn testコマンドでテストを実行し、テストが通ることを確認しましょう。

shell
yarn test
shell
yarn test

次のような結果になるはずです。

テストがPASSしているコンソールの画面

さて、次にボタンをクリックします。コンポーネントの操作はtesting-libraryに収録されている@testing-library/user-eventを使って実現できます。@testing-library/user-eventはコンポーネントの操作を含む、色々なユーザーイベントをテストで実行するライブラリです。具体的にはclick()にクエリでみつけたsimpleButtonを引数として渡すことで、ボタンのクリックを実現できます。

SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
const user = userEvent.setup();
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
await user.click(simpleButton);
});
SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
const user = userEvent.setup();
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
await user.click(simpleButton);
});

続けて、ボタンがクリックされた後のアサーションを実施します。先ほどと同様にtoHaveTextContent()を用いますが、今度はボタンのテキストがONになっていることを確認しましょう。

SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
const user = userEvent.setup();
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
await user.click(simpleButton);
expect(simpleButton).toHaveTextContent("ON");
});
SimpleButton.test.tsx
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SimpleButton } from "./SimpleButton";
 
test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
const user = userEvent.setup();
render(<SimpleButton />);
const simpleButton = screen.getByRole("button");
expect(simpleButton).toHaveTextContent("OFF");
await user.click(simpleButton);
expect(simpleButton).toHaveTextContent("ON");
});

この状態でyarn testコマンドでテストを実行し、テストが通ることを確認しましょう。次のような結果になるはずです。

テストがPASSしているコンソールの画面

以上が、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ステップから成ります。

  1. スナップショットを検証したい状態にコンポーネントを持っていく
  2. スナップショットに照合する

ここではボタンが描画されてまだ何も操作されていない状態、つまりボタンにOFFと表示されている状態についてスナップショットテストを実施することを考えます。描画されたばかりの状態を検証したいので、描画してすぐにスナップショット照合を行えばよいことになります。

この考えをもとに、実際のコードを書いてみましょう。コンポーネントの描画には@testing-library/reactrender関数を、スナップショットの照合にはJestのtoMatchSnapshot()関数をそれぞれ使用して次のように書くことができます。

SimpleButton.test.tsx
tsx
import { render } from "@testing-library/react";
import { SimpleButton } from "./SimpleButton";
 
test("描画されてすぐはOFFと表示されている", () => {
const view = render(<SimpleButton />);
expect(view.container).toMatchSnapshot();
});
SimpleButton.test.tsx
tsx
import { render } from "@testing-library/react";
import { SimpleButton } from "./SimpleButton";
 
test("描画されてすぐはOFFと表示されている", () => {
const view = render(<SimpleButton />);
expect(view.container).toMatchSnapshot();
});
備考

Jest単体ではReactコンポーネントの描画ができません。そこで、コンポーネントの描画をするためのライブラリを使用する必要があります。多くのライブラリがありますが、ここでは前章「testing-libraryを使ったテストの作り方とやり方」でも紹介した@testing-library/reactを用いました。

テストファイルが作成できたら、yarn testコマンドを実行します。

shell
yarn test
shell
yarn test

そうすると次のように表示され、テストが実行されて成功した(PASSした)ことがわかります。

SimpleButtonコンポーネントのテストがPASSした結果画面

さて、このときsrcディレクトリの中に__snapshots__というディレクトリが自動で追加されているはずです。これはJestがスナップショットテスト用のファイルを保存していくためのフォルダです。Jestのスナップショットテストは初回実行時にスナップショットテスト用のファイルを生成し、2回目から照合を行います。いまは初回実行だったため、ファイルとその置き場であるディレクトリが自動で生成されました。

ここでスナップショットテストについてもう少しだけ知るために、生成されたスナップショットテスト用のファイルの中身を覗いてみましょう。

__snapshots__ディレクトリの中に作られたSimpleButton.test.tsx.snapは次のようになっています。

SimpleButton.test.tsx.snap
js
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`描画されてすぐはOFFと表示されている 1`] = `
<div>
<button>
OFF
</button>
</div>
`;
SimpleButton.test.tsx.snap
js
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`描画されてすぐはOFFと表示されている 1`] = `
<div>
<button>
OFF
</button>
</div>
`;

このように、スナップショットテスト用のファイルはテストケースの名前と、そのテストケースで使われるスナップショットで構成されています。

さて、今回生成されたスナップショットはOFFというテキストを持ったbuttonタグと、その親要素であるdivタグで構成されています。これは、まさに先ほど作ったSimpleButtonコンポーネントのDOMに一致します(div要素はReactの起動時に自動生成される要素です)。このスナップショットテストは実行のたびに、SimpleButtonコンポーネントを描画して、たった今作られたこのスナップショットとの違いが生まれていないかを確認してくれます。
たとえば、もしも何かの手違いでSimpleButtonコンポーネントが描画されたときにONと表示されるようになっていたら、このスナップショットテストに引っかかるのです。

ここで、実際に失敗する様子も確認してみましょう。SimpleButtonコンポーネントが描画されたときにONと表示されるよう変更を加えます。

SimpleButton.tsx
tsx
import { useState } from "react";
 
export const SimpleButton: () => JSX.Element = () => {
const [state, setState] = useState(true);
// falseからtrueに変更 ^^^^
const handleClick = () => {
setState((prevState) => !prevState);
};
return <button onClick={handleClick}>{state ? "ON" : "OFF"}</button>;
};
SimpleButton.tsx
tsx
import { useState } from "react";
 
export const SimpleButton: () => JSX.Element = () => {
const [state, setState] = useState(true);
// falseからtrueに変更 ^^^^
const handleClick = () => {
setState((prevState) => !prevState);
};
return <button onClick={handleClick}>{state ? "ON" : "OFF"}</button>;
};

この状態でyarn startコマンドを実行すると、描画されたボタンの文字の初期値がONになっていることが分かります。

さて、ここでyarn testコマンドを実行します。

shell
yarn test
shell
yarn test

先ほどのスナップショットテストが実行されますが、今回はテストが通らず、描画されたコンポーネントとスナップショットの差分が表示されます。

SimpleButtonコンポーネントのテストがFAILし、描画されたコンポーネントとスナップショットの差分が表示されている結果画面

今回はボタン内テキストの初期値を変更しましたが、たとえばbuttonタグからdivタグへの変更やbuttonタグへのクラスの追加など、DOMに対する変更のほとんどをスナップショットテストで検知できます。

スナップショットテストの詳しいやり方やベストプラクティスなど、さらに詳しい情報に触れたい方はJestの公式ドキュメントをご参照ください。

以上でJestを使ったスナップショットテストのチュートリアルは完了です。また、Reactコンポーネントのテストのチュートリアルも完了です。