Jestを用いたReactアプリのテスト自動化
RGAでインフラエンジニアをしている加田です。
最近ある案件で、UI/UXチームと開発チームの取り持ちとして、UI/UXチームの要件が開発チームによる修正内容に正しく反映されているのかを確認するためのUIテストを行っています。
ひとまず手動でテストを行なってテスト結果をエクセルにまとめて提出したところ、Jestを用いて自動化できないかと持ちかけられたので、Jestを触った感触についてまとめてみることにしました。
Jestについて
概要
Jestの公式ページ ではJestを次のように説明しています。
出力やエラーが期待したものかを確認する単純なテストはもちろんのこと、スナップショットテストという、前回のテスト時からUIに予期せぬ変更が発生していないかを確認するテストも容易に行えるフレームワークとなっています。
今回はこのJestを使ってReactアプリのテストを構築します。
Jestのチュートリアル
Jestはyarnやnpmでインストールできます。
初期化処理を事前に行い、package.jsonを用意しておいてください。
yarn add --dev jest
npm install --save-dev jest
まずは単純な足し算のテストの構築と実行をしてみます。
デフォルトのJestでは、以下のどちらかの条件に当てはまったJavaScriptをテストコードとして認識します。
__test__ディレクトリの配下
拡張子が .spec.js または .test.js
今回は後者に従い、以下のコード(ファイル名: ./add.test.js)を作成します。
describe('test tutorial', () => {
test('test1', () => {
expect(1+2).toBe(3);
});
});
testで1つのテストケースを構築し、describeで似たようなテストケースをまとめた単位であるテストスイートを構築することができます。
expectで式の評価や関数の呼び出しを行い、目的に応じたMatcherを使うことで様々な検証ができます。ここでは等価性の検証に用いられるtoBeと呼ばれるMatcherを呼び出しています。
1+2を評価した値は3でtoBeの引数と一致するので、この検証結果は正しいはずです。
では、package.jsonに以下を記載し、npmやyarnからJestを使ったテストを実行できるようにします。
{
"scripts": {
"test": "jest"
}
}
最後にyarn testやnpm testでテストを実行すると、Jestは以下のメッセージを出力します。
PASS ./add.test.js
test tutorial
✓ test1 (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.381 s
Ran all test suites.
無事、テストが全て通ったことが確認できました。
Matcherにはテストの目的に応じて以下のように様々なものがあります。
文字列に対して正規表現でマッチすることを確認するtoMatch
配列やリスト等に特定のアイテムが含まれていることを確認するtoContain
関数を呼び出した時に例外を投げることを確認するtoThrow
詳しくはExpectに関する公式ドキュメントを参照してください。
Reactアプリにおける各種テスト
続いて、Reactアプリを対象としたテストを実装してみます。
まずはReactプロジェクトを作成するために、任意のディレクトリで以下のコマンドを実行します。
npx create-react-app my-app
create-react-appは必要な様々なパッケージをインストールしてReactプロジェクトの土台を作成してくれますが、インストールされるパッケージの中にはJestも含まれているため先程のような個別でのインストール処理は不要です。
スナップショットテスト
まず、単純なコンポーネントでスナップショットテストの構築と実行をしてみます。
テスト対象とするReactコンポーネントは以下とします。
・my-app/src/Components/Main.js
import React from 'react';
class Main extends React.Component {
render() {
return <h1>Hello world!</h1>;
}
}
export default Main;
h1タグで「Hello world!」を表示するだけのシンプルなコンポーネントです。
続いて、このコンポーネントに対してスナップショットテストを行うためのテストコードを作成します。
その前にスナップショットテストを行う際に必要なパッケージであるreact-test-rendererをインストールしておきましょう。
yarn add --dev react-test-renderer
npm install --save-dev react-test-renderer
これで必要なパッケージがインストールできたので、改めてテストコード(my-app/src/__test__/Main.test.js)を作成すると以下のようになります。
import renderer from 'react-test-renderer'
import Main from '../Components/Main'
describe('snapshot-test tutorial', () => {
test('test2', () => {
const tree = renderer
.create(<Main />)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
コードの中ではまず、対象となるコンポーネントをレンダリングしたものをJSON化しています。
これをexpectでテスト対象に指定して、toMatchSnapshotというMatcherを用いています。
yarn testやnpm testでテストを実行すると、Jestは以下のメッセージを出力します。
PASS src/__test__/Main.test.js
› 1 snapshot written.
PASS src/App.test.js
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 1 written, 1 total
Time: 2.636 s
Ran all test suites.
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› 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.
テスト実行直後はWatchモードとなりますが、ここではqを押してWatchモードを終了させてください。
src/App.test.jsはcreate-react-app実行時に自動的に作成されたものなので無視します。
src/__test__/Main.test.jsのテスト実行時にスナップショットが作成されたというメッセージがあることが分かります。
プロジェクトの中身を見に行くと、src/__test__内に新しく、__snapshots__というディレクトリと、そのディレクトリ直下にMain.test.js.snapというファイルが出来ていることがわかります。
このファイルがスナップショットであり、Main.test.js.snapの中身は以下のようになっています。
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot-test tutorial test2 1`] = `
<h1>
Hello world!
</h1>
`;
コンポーネントのレンダリング結果に加え、テストスイートやテストケースの情報が含まれていることが分かります。
次に、再度yarn testやnpm testでテストを実行すると、Jestは以下のメッセージを出力します。
PASS src/__test__/Main.test.js
PASS src/App.test.js
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 2.167 s
Ran all test suites.
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› 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.
今度はスナップショットが作成された旨のメッセージが出てこず、テストスイートやテストがパスされるとともにスナップショットもパスされていることが分かります。
続いて、コンポーネント(my-app/src/Components/Main.js)に手を加え、表示されるメッセージを「Hello world!」から「Hello jest!」に書き換えてみます。
import React from 'react';
class Main extends React.Component {
render() {
return <h1>Hello jest!</h1>;
}
}
export default Main;
この状態で再度yarn testやnpm testでテストを実行すると、Jestは以下のメッセージを出力します。
PASS src/App.test.js
FAIL src/__test__/Main.test.js
● snapshot-test tutorial › test2
expect(received).toMatchSnapshot()
Snapshot name: `snapshot-test tutorial test2 1`
- Snapshot - 1
+ Received + 1
<h1>
- Hello world!
+ Hello jest!
</h1>
7 | .create(<Main />)
8 | .toJSON();
> 9 | expect(tree).toMatchSnapshot();
| ^
10 | });
11 | });
at Object.<anonymous> (src/__test__/Main.test.js:9:18)
› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 1 failed, 1 total
Time: 2.409 s
Ran all test suites.
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press u to update failing snapshots.
› Press i to update failing snapshots interactively.
› 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.
スナップショットテストが失敗し、「Hello world!」が「Hello jest!」に書き換えられたことが分かるメッセージが表示されています。
また、Watchモードで選べるコマンドの中にスナップショットに関するものが増えていることも分かります。
qを押すと既存のスナップショットを残したままWatchモードを終了させることができます。
逆に、uを押すとスナップショットは上書きされ、今後はMainコンポーネントのスナップショットテストには「Hello jest!」のものが用いられるようになります。
このように、スナップショットテストを実装することで、以前のレンダリング結果をスナップショットとして保存し、コンポーネントの見た目上での変化を調べることができるようになります。
UIテスト
スナップショットテストによりReactアプリにおけるUIの見た目をテストできるようになりました。
では最後に、クリックや入力といったUIの機能面のテストを実装してみます。
先程スナップショットテストに用いたプロジェクトを使い回します。
テスト結果の表示が増えて邪魔なのでsrc/App.test.jsとsrc/__test__/Main.test.jsは削除してしまってください。
クリックと入力の動作を含む、以下のコンポーネント(src/Components/Sub.js)を実装してみます。
import React from 'react';
class Sub extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "デフォルト",
changedTitle: ""
}
this.handleChange = this.handleChange.bind(this)
this.handleClick = this.handleClick.bind(this)
}
handleChange(e) {
this.setState({
changedTitle: e.target.value
});
}
handleClick(e) {
this.setState({
title: this.state.changedTitle,
changedTitle: ""
})
}
render() {
const { title, changedTitle } = this.state;
return (
<div>
<h1>{title}</h1>
<input
type="text"
value={changedTitle}
onChange={this.handleChange}
/>
<button
type="button"
data-testid="executeButton"
onClick={this.handleClick}>
変更
</button>
</div>
);
}
}
export default Sub;
これを開くと次のようなページになります。
下のフォームに入力して「変更」ボタンをクリックすると、最初は「デフォルト」となっている上の字が入力した文字列に変わる単純なアプリとなっています。
続いて、このアプリのUIテストを実行するためのコード(src/__test__/Sub.test.js)を作成してみます。
import { render, screen } from '@testing-library/react'
import user from "@testing-library/user-event"
import Sub from '../Components/Sub'
describe('UI-test tutorial', () => {
test('test3', () => {
render(<Sub />);
const headerTitle = screen.getByText("デフォルト");
const titleInput = screen.getByRole("textbox", { name: "" });
const executeButton = screen.getByTestId("executeButton");
user.type(titleInput, "変更後");
user.click(executeButton);
expect(headerTitle).toHaveTextContent("変更後");
})
})
新たに@testing-libraryというパッケージを使っていますが、これはcreate-react-appによって自動的にインストールされているため、手動でインストールする必要なく用いることができます。
ここではまず、renderによりSubコンポーネントをレンダリングし、その表示内容をscreenにより取り出しています。
取り出した内容は先程載せたスクリーンショットを表現するDOMになっています。
ここにgetBy*というクエリを用いることで各要素を個別に取り出すことができます。
最初のscreen.getByText("デフォルト")は「デフォルト」という文字を含む要素を取り出すクエリとなっており、ここではタイトルのh1タグ要素を取り出しています。
次のscreen.getByRole("textbox", { name: "" });はtextタイプでname属性を設定していないinput要素を取り出すクエリとなっており、入力フォームを取り出しています。
最後のscreen.getByTestId("executeButton")はdata-testid属性を指定して要素を取り出すクエリとなっており、「変更」ボタンを取り出しています。
取り出した要素に対して、userを用いることで入力やクリックといった各種イベントを発生させることができます。
例えば今回のテストコードでは、user.type(titleInput, "変更後")によってフォームに「変更後」と入力をし、user.click(executeButton)によって「変更」ボタンをクリックさせています。
最後にexecuteとMatcherを用いて検証を行ないます。
今回は「変更」ボタンによりタイトルが正しく書き換わるかを検証するために、検証対象は最初に取り出したh1タグ要素であるheaderTitleとし、Matcherには検証対象に特定の文字列が含まれるかを確認するtoHaveTextContentを用います。
最初の「デフォルト」ではなく、フォームに入力した「変更後」に変わっていて欲しいので、toHaveTextContentの引数は"変更後"としておきます。
テストコードも実装できたので、yarn testやnpm testでテストを実行すると、Jestは以下のメッセージを出力します。
PASS src/__test__/Sub.test.js
UI-test tutorial
✓ test3 (99 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.131 s
Ran all test suites.
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› 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.
無事テストがパスされ、h1要素の中身が「変更後」に書き換わっていることが確認できました。
このように、Jestを使って各要素に対するクリックや入力といった操作を記述することで、その順番通りに操作を行なった状況をシミュレートでき、自動化されたUIテストを実装することができるようになります。
まとめ
今回はJestを使った基本的なテストの構築手法と、Reactアプリにおいてどういったテストができるのかについて見ていきました。
以前にUiPathというGUIを用いたテストの自動化を経験したことがありますが、Jestはアプリを起動させる必要がなく、コンポーネント単位でテストができる点が強みだと思いました。
参考文献
__________________________________
【ご案内】
ITシステム開発やITインフラ運用の効率化、高速化、品質向上、その他、情シス部門の働き方改革など、IT自動化導入がもたらすメリットは様々ございます。
IT業務の自動化にご興味・ご関心ございましたら、まずは一度、IT自動化の専門家リアルグローブ・オートメーティッド(RGA)にご相談ください!
お問合せは以下の窓口までお願いいたします。
【お問い合わせ窓口】
代表窓口:info@rg-automated.jp
URL: https://rg-automated.jp