React Testing Libraryとjestを使ってReactアプリケーションのテストをしてみた
こんにちわ、山崎です。久々の投稿になります。
今まで自動テストをやってこなかったので、「React Testing Library」と「jest」を組み合わせてReactアプリケーションのテストをする方法を学んだので、アプリケーションテストの簡単なサンプルを紹介したいと思います。
create-react-appでReactアプリの雛形を作成する
まずcreate-react-app
でReactアプリケーションの雛形を作成します。
(今回仕様したnode.jsのバージョンはv15.14.0
になります。)
npx create-react-app rts
テスト対象のコンポーネントを作成する
そして、次にテストするReactのコンポーネントを作成します。 今回は超シンプルなToDoアプリを作成しました。
まず、src
ディレクトリにTodo.js
を作成します。
import React from "react";
const Todo = () => {
const [input, setInput] = React.useState('');
const [list, setList] = React.useState(['タスクA', 'タスクB', 'タスクC']);
const updateValue = (e) => {
setInput(e.target.value);
}
const addNew = () => {
let copiedList = [...list];
copiedList.push(input);
setList(copiedList);
setInput('');
}
return (
<div className="todo">
<input type="text" onChange={updateValue} value={input} />
<button disabled={!input} onClick={addNew}>Todoを追加</button>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)
}
export default Todo;
今回はあくまでテストの記事なのでコードの説明は省略しますが、仕様としてはテキストボックスを入力したらボタンのdisabledが解除されボタンを押したらTodoのリストに入力した内容が追加され、入力した内容がクリアされるという超シンプルなものです。
テストファイルを作成する
次に上記で作成したコンポーネントに対するテストファイルを作成していきます。
create-react-app
でインストールした雛形には標準でテストが搭載されています。
Todo.js
のコンポーネントに対するテストファイルはTodo.test.js
という名前で作成します。
まず最初に必要なツールをimportしましょう。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Todo from './Todo';
まず、最初にreactをimportします。
import React from 'react';
そして次に、@testing-library/react
からrender
関数とscreen
関数をimportします。
'@testing-library/user-event
からrenderしたDOM要素に対してイベントを発火させるuserEvent
をimportします。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
そして最後に先ほど作成したコンポーネントをimportします。
import Todo from './Todo';
これでテストの準備が整いました。
ここから実際にテストを書いていきます。
テスト1
まず、初期状態でボタンがdisabledになっているかテストを書いていきます。
describe('Todoコンポーネントテスト', () => {
it('初期状態でdisabledになっているか', () => {
render(<Todo />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
});
describe
というブロックの中に入れ子でit
関数を使ってそれぞれテストケースを書いていきます。
it
関数の第一引数にはテストケースの名前を定義し、第二引数にテストで行う一連の処理を記述します。
まず、第一にコンポーネントをレンダーします。
render(<Todo />);
そしてレンダーしたコンポーネント内の要素にアクセスするにはReact Testing Library
のscreen
を使用します。
getByRole
メソッドの引数にbutton
を使用するとレンダーしたDOMの最初のbuttonタグの要素にアクセスすることができます。
(ボタン要素が複数ある可能性があるので本来であればid指定などで要素を取得した方が良いです。)
const button = screen.getByRole('button');
次にexpect関数を使って実際にテストをします。
expect(button).toBeDisabled();
仕組みとしてはとてもシンプルでexpect
関数の第一引数に検証したい値を代入し、
その後に検証したい内容に応じて、メソッドを呼び出します。
上記の例ではexpect
の引数に入れたボタン要素がdisabledになっているかを検証しています。
テストケースが1つできたので、実際にこのテストを実行してみます。 Reactプロジェクトのルートディレクトリで、以下のコマンドをうちます。
npm run test
すると下記の通り、テストが実行され、1つのテストがパスしているのが分かります。
逆にこのテストがちゃんと正しく動作しているか確かめる為にあえて下記のようにボタンのdisabled属性を外してしまいます。
<button onClick={addNewTodo}>Todoを追加</button>
すると、Recieved element is not disabled
となっている通り、
button要素がdisabledになっていないので、テストが失敗していることが分かりました。
テスト2
次にまたit
関数を使用して別のテストを書いていきます。
inputに値を入力すると、ボタンのdisabledが解除されるかどうかをテストします。
it('inputに値を入力するとボタンのdisabledが解除されるか', () => {
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
userEvent.type(input, 'hoge');
expect(button).not.toBeDisabled();
});
まず、先ほどのテストと同じように要素を取得します。inputタグの場合はテキストボックスの分類になるので、
screen.getByRole('textbox')
で取得することができます。
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
そしていよいよuserEvent
の出番です。
type
メソッドを使用することで、inputタグに値を入力することができます。
第一引数にイベントを発火させる要素、第二引数に入力する文字列を加えます。
userEvent.type(input, 'hoge');
そして、OOでないことを検証したい場合は下記のように、
expectのメソッドの前に.not
とつけます。
expect(button).not.toBeDisabled();
こうすることによってボタンのdisabledが解除されているか検証できるようになりました。
テスト3
最後に、ボタンをクリックした後に、Todoがリストに追加されるかを検証します。
it('ボタンをクリックするとTodoが追加されるか', () => {
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
const listItem = screen.getAllByRole('listitem');
userEvent.type(input, 'hoge');
userEvent.click(button);
const listItemAfterClicked = screen.getAllByRole('listitem');
expect(listItemAfterClicked.length).toBeGreaterThan(listItem.length);
expect(listItemAfterClicked[listItemAfterClicked.length - 1].textContent)
.toEqual('hoge');
});
まず、使用する要素を定義します。liは複数存在するので、getAllByRole('listitem')
とすることで要素の配列を取得することができます。
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
const listItem = screen.getAllByRole('listitem');
次にinputに値を入力した後にボタンをクリックします。
click
メソッドでは第一引数にクリックしたい要素をいれるのみです。
userEvent.type(input, 'hoge');
userEvent.click(button);
そして、改めてscreen.getAllByRole('listitem')
でli要素を取得し、
toBeGreaterThan
メソッドでボタンクリック前のli要素の数とクリック後のli要素の数を比較しています。
expectで渡した数値が上回っていると期待れる数値をtoBeGreaterThan
要素の引数に加えます。
const listItemAfterClicked = screen.getAllByRole('listitem');
expect(listItemAfterClicked.length).toBeGreaterThan(listItem.length);
そして、最後のli要素がhoge
であることも検証する場合は下記のようにします。
toEqual
はexpectで渡した文字列(もしくは数値)が引数と一致するかを検証します。
expect(listItemAfterClicked[listItemAfterClicked.length - 1].textContent)
.toEqual('hoge');
最後に
下記がTodo.test.js
の全コードになります。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Todo from './Todo';
describe('Todoコンポーネントテスト', () => {
it('初期状態でボタンがdisabledになっているか', () => {
render(<Todo />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
it('inputに値を入力するとボタンのdisabledが解除されるか', () => {
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
userEvent.type(input, 'hoge');
expect(button).not.toBeDisabled();
});
it('ボタンをクリックするとTodoが追加されるか', () => {
render(<Todo />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
const listItem = screen.getAllByRole('listitem');
userEvent.type(input, 'hoge');
userEvent.click(button);
const listItemAfterClicked = screen.getAllByRole('listitem');
expect(listItemAfterClicked.length).toBeGreaterThan(listItem.length);
expect(listItemAfterClicked[listItemAfterClicked.length - 1].textContent)
.toEqual('hoge');
expect(input.value)
.toEqual('');
});
});
今回は非常にシンプルなアプリケーションだった為、このサンプルではテストの恩恵があまり感じられないかと思いますが、 テストケースが多かったり複雑だったりするパターンや、コードが複雑でちょっといじっただけで動かなくなってしまったっといったことを事前に防ぐ為にも今後はしっかり自動テストを活用していけたらと思います。