坂本研のゼミ室

create-react-appで作成したReactアプリをテストしてみる

はじめに

宮崎大学 Advent Calendar 2019の記事です。
qiita.com

ソースコードはこちら。
github.com


テストって何?

プログラムの振る舞いを確認し品質を保証する目的で行う。
手動はつらいので、『こういう入力があったらこういう動作』をあらかじめ記述しテストフレームワークに任せる事で自動化する。

Jest

  • Facebookが開発
  • JavaScriptユニットテストツール
  • __tests__フォルダ内のファイルまたは、ファイル名が◯◯.spec.js, ◯◯.test.jsに対してテストを実行する
  • srcフォルダのsetupTests.jsから設定を読み込む
  • create-react-appにはあらかじめJestが同梱されている
  • 日本語ドキュメントもあるので詳しく知りたい人はぜひ

jestjs.io

Enzyme

airbnb.io

環境構築

$ node -v
v11.10.1

新しいReactアプリを作る

npx create-react-app my-app

以下のようなファイル構成になっている
f:id:TakaShinoda:20191105050011p:plain

プロジェクトを実行

npm start

実際にテストしてみる

まずは、App.jsを以下のように書き換える

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div>
        <h1>みやだいもうくん</h1>
      </div>
    );
  }
}

export default App;

次にApp.test.jsを書き換える(.test.jsにテストを記述していく)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

test('innerHTMLに含まれているか確認', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  //<App />内のinnerHTMLに「みやだいもうくん」が含まれているかどうかをテストする
  //”toContain”は含まれているかどうかをチェックする関数
  expect(div.innerHTML).toContain('みやだいもうくん');
  ReactDOM.unmountComponentAtNode(div);
});

テストを実行してみる。

npm test

このようになれば成功。テストが通りました。
f:id:TakaShinoda:20191120232731p:plain

Jestによるテストの書き方メモ

test("最初のテスト", () => {
    expect(1 + 1).toEqual(2);
});
/** test("テストの名前", () => {});
 * 第1引数: テストにつける名前
 * 第2引数: テストに実行して欲しい内容をアロー関数で記述
 * 
 * テスト (test) を宣言
 * テストの名前は「最初のテスト」
 * 実行してほしい内容は(1 + 1) の結果が、(2) と等しくなること (toEqual) を期待 (expect) します
 */

テスト実行のながれ

f:id:TakaShinoda:20191121003115p:plain

コンポーネントの存在を確認

ます、子コンポーネントとしてCount.jsxを作成

import React, { Component } from 'react';

class Count extends Component {
    constructor(props){
        super(props);
        this.state = {
            count: 0
        }
        this.doAdd = this.doAdd.bind(this);
    }

    doAdd() {
        this.setState({
            count: this.state.count +1
        });
    }

    render() {
        return (
            <div>
                <button onClick={this.doAdd}>+1ボタン</button>
                <p>{this.state.count}</p>
            </div>
        );
    }
}

export default Count;

次に、enzymeとenzyme-adapter-react-16をインストール
(16はreactのバージョン)

npm install enzyme enzyme-adapter-react-16

srcフォルダ内にsetupTests.jsを作成し以下のように記述する
(Jestはsrc/setupTests.jsを探して設定を読み込む)

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

App.test.jsにテストを追加する

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Count from './Count';
import { shallow, mount, render } from 'enzyme';

it('innerHTMLに含まれているか確認', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  //<App />内のinnerHTMLに「みやだいもうくん」が含まれているかどうかをテストする
  //”toContain”は含まれているかどうかをチェックする関数
  expect(div.innerHTML).toContain('みやだいもうくん');
  ReactDOM.unmountComponentAtNode(div);
});

test('子コンポーネントが存在するか確認', () => {
  /** Appコンポーネントをshallowレンダリング */
  const wrapper = shallow(<App />);

  /** 各コンポーネントの数を取得し、1であればOK */
  expect(wrapper.find(Count).length).toBe(1);
});

setStateで値が更新されているか確認

名前を入力して表示するName.jsxを作成

import React, { Component } from 'react';

class Name extends Component {
    constructor(props){
        super(props);
        this.state = {
        name: 'hoge'
        }
        this.onChange = this.onChange.bind(this);
    }

    onChange(e) {
        this.setState({
            name: e.target.value
        });
    }

    render() {
        return (
            <div>
                <input type='text' onChange={this.onChange} placeholder="名前を入力" />
                <p>こんにちは! {this.state.name}さん</p>
            </div>
        );
    }
}

export default Name;


Name.test.jsを作成して、Name.jsxのテストを記述する

import React from 'react';
import Name from './Name';
import { shallow } from 'enzyme';

test('setStateで<Form>のnameが変更されるかを確かめる', () => {
    const wrapper = shallow(<Name />);
    /** <Form />のstate.nameが’’ではない。(初期stateは{name: ‘hoge’}) */
    expect(wrapper.state().name).not.toEqual('')
    /** setStateでnameをみやだいもうくんに更新 */
    wrapper.setState({ name: 'みやだいもうくん'});
    /** state.nameがみやだいもうくんならOK */
    expect(wrapper.state().name).toEqual('みやだいもうくん')
});

コンポーネントで受け取ったpropsがレンダリングされているか確認

App.jsからpropsを受け取る子コンポーネントとしてMessage.jsxを作成
今回はテキストを受け取るだけです。
App.jsに追加する。

import React, { Component } from 'react';
import Count from './Count';
import Name from './Name';
import Message from './Message';

class App extends Component {
  render() {
    return (
      <div>
        <h1>みやだいもうくん</h1>
        <Count />
        <Name />
        <Message message = '宮崎へ' />
      </div>
    );
  }
}

export default App;

Message.jsx

import React, { Component } from 'react';

class Message extends Component {
    render() {
        return (
            <div>
                <p>ようこそ! {this.props.message}</p>
            </div>
        );
    }
}

export default Message;


Message.test.js

import React from 'react';
import Message from "./Message";
import { shallow } from 'enzyme';

test('受け取ったpropsの値を表示できているか確認', () => {
    /*'佐賀へ'という値をtextに渡して、Messageコンポーネントをshallowレンダリング*/
    const wrapper = shallow(<Message message={'佐賀へ'} />);
    /** レンダリングされたテキストが'ようこそ! 佐賀へ'であればOK */
    expect(wrapper.text()).toBe('ようこそ! 佐賀へ');
    /** props.messageの値を'World'に変更 */
    wrapper.setProps({ message: 'World' });
    /** レンダリングされたテキストが'ようこそ! World'であればOK */
    expect(wrapper.text()).toBe('ようこそ! World');
});

モックを使ったテスト

モックを使う事で、乱数を使ったテストなどの実際には実行してほしくないテストの際に代わりとなる値を返すことができます。

アラートのテスト

最後に、アラート表示のテストを行います。
Alert.jsxを作成

import React, { Component } from 'react';

class Alert extends Component {
    constructor(props){
        super(props);
        this.state = ({
            notification: 'himuka'
        });
        this.onClick = this.onClick.bind(this);
    }

    onClick() {
        alert(this.state.notification);
    }

    render() {
        return (
            <div>
                <button onClick={this.onClick}>
                    アラート
                </button>
            </div>
        );
    }
}

export default Alert;

Alert.jsxのテストを記述するAlert.test.jsを作成

import React from 'react';
import Alert from './Alert';
import { shallow } from 'enzyme';

test('アラート表示されているか確認', () => {
  /** モック関数を作成 */
  window.alert = jest.fn();
  const wrapper = shallow(<Alert onClick={window.alert('hogehoge')} />);
  /** クリックイベントをシュミレート */
  wrapper.simulate('click');
  /** モック関数が1回呼び出される */
  expect(window.alert.mock.calls.length).toBe(1)
  /** toHaveBeenCalledWithは特定のFunctionが特定の引数で呼び出されたかを検証 */
  expect(window.alert).toHaveBeenCalledWith('hogehoge');
});