React Testing Libraryとjestを使ってReactアプリケーションのテストをしてみた

No Photo

こんにちわ、山崎です。久々の投稿になります。

今まで自動テストをやってこなかったので、「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 Libraryscreenを使用します。 getByRoleメソッドの引数にbuttonを使用するとレンダーしたDOMの最初のbuttonタグの要素にアクセスすることができます。 (ボタン要素が複数ある可能性があるので本来であればid指定などで要素を取得した方が良いです。)

const button = screen.getByRole('button');

次にexpect関数を使って実際にテストをします。

expect(button).toBeDisabled();

仕組みとしてはとてもシンプルでexpect関数の第一引数に検証したい値を代入し、 その後に検証したい内容に応じて、メソッドを呼び出します。 上記の例ではexpectの引数に入れたボタン要素がdisabledになっているかを検証しています。

テストケースが1つできたので、実際にこのテストを実行してみます。 Reactプロジェクトのルートディレクトリで、以下のコマンドをうちます。

npm run test

すると下記の通り、テストが実行され、1つのテストがパスしているのが分かります。 スクリーンショット 2021-09-29 21.28.02.png

逆にこのテストがちゃんと正しく動作しているか確かめる為にあえて下記のようにボタンのdisabled属性を外してしまいます。

<button onClick={addNewTodo}>Todoを追加</button>

すると、Recieved element is not disabledとなっている通り、 button要素がdisabledになっていないので、テストが失敗していることが分かりました。

スクリーンショット 2021-09-29 21.35.29.png

テスト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('');
  });
});

今回は非常にシンプルなアプリケーションだった為、このサンプルではテストの恩恵があまり感じられないかと思いますが、 テストケースが多かったり複雑だったりするパターンや、コードが複雑でちょっといじっただけで動かなくなってしまったっといったことを事前に防ぐ為にも今後はしっかり自動テストを活用していけたらと思います。

  • このエントリーをはてなブックマークに追加

この記事を読んだ人にオススメ