15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

jestとreact-testing-libraryでreactのテストをする

Posted at

はじめに

1回目のReactを使用してWeb画面を作成するでは、reactの簡単なサンプルと共に説明をまとめました。
2回目のReduxとReduxToolkitを使用してReact内でデータを管理するでは、reduxのの簡単なサンプルと共に説明をまとめました。
3回目のReactのRedux内でaxiosを使用した通信をするでは、axiosを使用してREST APIから情報を取得するサンプルと説明をまとめました。
今回は、jestとreact-testing-libraryで仮想DOMのテストをしてみます。

環境

  • node.js: v12.18.2
  • webpack: 4.44.1
  • React: 16.13.1
  • Redux: 7.2.1
  • axios: 0.19.2
  • jest: 26.3.0
  • react-testing-library: 10.4.8

環境作成

今までの続きとなります。
環境構築などはそちらを見てください。

jest

テストをするために、その枠組みを提供するjestというライブラリを使用します。

jestのインストール


yarn add --dev babel-jest react-test-renderer
npm install -g jest

react-testing-library

reactが生成する仮想DOMの操作をするためにreact-testing-libraryというライブラリを使用します。

react-testing-libraryのインストール


npm install --save-dev @testing-library/react

axios-mock-adapter

axiosを使用して通信を行っていますが、テストのたびにREST APIのサーバを起動するのは手間なのでモックを使用してaxiosの処理を置き換えます。

axios-mock-adapterのインストール


npm install --save-dev axios-mock-adapter

テストソースを作成する

テストソースを入れるフォルダの作成

テスト用ソースを入れるフォルダとしてルートフォルダの直下にtestフォルダを作成します。


project_root
├─dist   // ビルド後のファイルを格納 
├─public // htmlを格納
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─test    // reactのテストファイルを格納

テストファイルの作成

jestは×××.spec.jsや×××.test.jsというファイルを読み込んで実行するため、それに沿ったファイル名にしてください。

Message.spec.js

import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

const mockAxios = new MockAdapter(axios);

mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
// テスト実行後にDOMをunmount, cleanupします
afterEach(cleanup)

describe("Messageテスト", () => {
    it("初期値:メッセージが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // メッセージが2つ(テキストとボタンにあるか)
        expect(messageRender.getAllByText("メッセージ")).toHaveLength(2);
        // こんにちはが1つ(ボタンにあるか)
        expect(messageRender.getAllByText("こんにちは")).toHaveLength(1);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("通信")).toHaveLength(1);
        // ボタンが2つあるか)
        expect(messageRender.getAllByRole("button")).toHaveLength(3);
    });

    it("メッセージを押すと入力したものが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // こんにちはが1つ(ボタンにあるか)
        const inputElement = messageRender.getByRole("textbox");
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // こんにちはが1つ(ボタンにあるか)
        const messageElement = messageRender.getAllByText("メッセージ");
        // 通信が1つ(ボタンにあるか)
        fireEvent.click(messageElement[0]);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
    });

    test("通信のテスト", async () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // 通信ボタンを取得
        const messageElement = messageRender.getAllByText("通信");
        // 通信ボタンをクリック。失敗すればエラーになる
        await fireEvent.click(messageElement[0]);
    });
});

jestの形式

jestは指定されたフォーマットにしたがって書くことによってテストの実行をしてくれます。

Message.spec.js

インポート

// afterEach内の関数はテスト後に実行される
afterEach(cleanup)

// テストの塊
describe("Messageテスト", () => {
    // テスト
    it("初期値:メッセージが表示される", () => {
        ~~~ テストの内容 ~~~ 
    });
});

jestのフォーマットはこのような形になります。他にもテスト前やテストスイート前後にする処理を登録できます。

仮想DOMの生成

仮想DOMの生成にはreact-testing-libraryのrenderを使用します。この中に、テストしたいアプリのソースを登録します。
今回はMessageコンポーネントのテストをしたいのでrenderに入れていますが、storeも使っているためそれも適用しています。

Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

        ~~~ 省略 ~~~ 

        // 仮想DOMの生成
        const messageRender = render(<Provider store={store}><Message /></Provider>);
    });

仮想DOMの操作

生成した仮想DOMに対してgetByXXXやfindByXXXといった関数を実行すると仮想DOM内の要素を抜き出すことができます。要素のイベントを発火させるためにはfireEvent.change(DOMElement);で発火させます。
今回は入力するためにgetByRole("textbox")で抜き出して、値を入れた後にfireEvent.change(inputElement);で反映させています。
さらにボタンを押すためにgetAllByText("メッセージ")で抜き出した後に、メッセージという文字列が2つあり最初の要素がボタンであるため、そっちのイベントを発火させます。

Message.spec.js
        ~~~ 省略 ~~~ 

        // 入力するテキストボックスを取得
        const inputElement = messageRender.getByRole("textbox");
        // テキストの入力と反映。
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // ボタンの取得
        const messageElement = messageRender.getAllByText("メッセージ");
        // ボタンのクリック
        fireEvent.click(messageElement[0]);
        // ボタンを押した結果を確認
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
        ~~~ 省略 ~~~ 

axiosのモックを生成

モックを使用するときは、MockAdapterを使用して、モックを生成した後に、モック内の処理を記載します。
今回はgetのリクエストを置き換えるのでonGet(URL).reply(httpステータス,レスポンスボディ)を使用することにより、モック内の処理を定義します。

Message.spec.js
        ~~~ 省略 ~~~ 
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

// mockの生成
const mockAxios = new MockAdapter(axios);

// mock内容を定義
mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
        ~~~ 省略 ~~~ 

テストソースの実行

プロジェクトルートでjestコマンドを実行することでテストできます。


jest

終わりに

厳密にテストする場合、action内のテストやstateのテストといった細かい単位でテストすることが可能になります。
それによってテスト結果がNGになった場合に、原因となる場所が限定されるので調査がしやすくなったり、修正後のテストの範囲を限定できたりと様々なメリットがあります。
しかし、そこら辺のテストをちゃんと書くにはReactのことを理解する必要がある上に時間がかかってしまうため、
Reactへのなじみのない人が実施すると時間だけがかかってしまい、テストソースがバグだらけとなったりしてしまいます。
厳密にテストするか大きな枠でテストするかは理解度と時間、テストの影響のトレードオフかなと思っています。

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?