enzyme+mocha+power-assertでReactコンポーネントのフルレンダリングテスト

公開されているReactコンポーネントのテストコードを見てみると、enzymeを使ってテストしているものを結構見かけます。enzymeはReact公式でも押しているっぽいので、積極的に使っていきたいです。

Note:
Airbnb has released a testing utility called Enzyme, which makes it easy to assert, manipulate, and traverse your React Components’ output. If you’re deciding on a unit testing library, it’s worth checking out: http://airbnb.io/enzyme/

日本語の紹介記事では@syossan27さんの記事が参考になりました。

ReactのテストをEnzymeで書いてみよう - Qiita

上記の記事では、コンポーネントのshallowレンダリングでのテストが実施されています。shallowレンダリングでは、全てのライフサイクルイベントが呼ばれないので、それらが必要な場合はフルレンダリングした状態でテストを実施する必要があります。


という訳で、表題にある通り、enzyme+mocha+power-assertでReactコンポーネントのフルレンダリングテストを実施するまでを書いていきたいと思います。

前提

  • browserifywebpack等のビルド環境は整ってる
  • 今回対象したのはbrowserify+babelify
  • 使用するReactのバージョン
    • react v15.2.0
    • react-dom v15.2.0

ビルド周りは特に関係無いと思いますが念のため。既に対象となるコンポーネントがコーディング済み、という前提で進めたいと思います。

テスト対象のコンポーネント

対象とするのは、通知などに使われるToastコンポーネントを想定したいと思います。仮コンポーネントの為、雑な作りとなっている点は大目に見てやってください。

import React, { Component, PropTypes } from "react";

export default class Toast extends Component {
  static propTypes = {
    children: PropTypes.node,
    onRequestClose: PropTypes.func
  };

  constructor(props) {
    super(props);
    this.handleClose = this.handleClose.bind(this);
  }

  componentDidMount() {
    this.timer = setTimeout(this.handleClose, 3000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
    this.timer = null;
  }

  handleClose() {
    const { onRequestClose } = this.props;

    if (typeof onRequestClose === "function") {
      onRequestClose();
    }
  }

  render() {
    return (
      <div className="toast">
        <button className="toast__close" onClick={this.handleClose}>
          Close!
        </button>
        <div className="toast__body">{this.props.children}</div>
      </div>
    );
  }
}

以下の様な動作を期待します。

  • マウント後、3秒後にonRequestCloseコールバックを呼び出す
  • button要素のクリックで同じくonRequestCloseコールバックを呼び出す

ファイル/ディレクトリ構成

ここまでで以下の様なディレクトリ構成となっています。

.
├── dist          #bundleファイル
│   └── index.js
├── package.json
└── src           #各コンポーネントファイル等
    ├── Toast.js  #テスト対象のToastコンポーネント
    └── index.js  #エントリーファイル (本記事では未使用)

今回使用したファイルは以下のリポジトリにあるので、記事に書かれていない箇所で不明な点があればご確認ください。

tsuyoshiwada/enzyme-sample
https://github.com/tsuyoshiwada/enzyme-sample

テストに必要なパッケージのインストール

必要なパッケージをインストールしていきます。

$ npm i -D mocha enzyme react-addons-test-utils jsdom babel-register power-assert

それぞれ以下のバージョンがインストールされました。

{
  "devDependencies": {
    "babel-register": "^6.9.0",
    "enzyme": "^2.3.0",
    "jsdom": "^9.4.0",
    "mocha": "^2.5.3",
    "power-assert": "^1.4.1",
    "react-addons-test-utils": "^15.2.0"
  }
}

react-addons-test-utilsは直接使用しませんが、インストールしておかないとenzyme実行時に怒られます。

あと、onRequestCloseが呼ばれたかどうか、3秒後に◯◯する、などの処理を簡単にテストしたいのでsinonもインストールしておきます。これは表題に直接関係無いのでお好みで。

$ npm i -D sinon

テストのセットアップ

testディレクトリを作成後、jsdomの設定ファイルとテストファイルを追加します。

$ mkdir test
$ touch test/.setup.js test/Toast.spec.js

ここまでで以下のファイル構成です。

.
├── dist
│   └── index.js
├── package.json
├── src
│   ├── Toast.js
│   └── index.js
└── test
    ├── .setup.js
    └── Toast.spec.js

npm scriptsへtestを追加

$ npm testを叩いたら、テストが実行されるように設定します。

"scripts": {
  "test": "mocha test/**/*.spec.js -r test/.setup.js --compilers js:babel-register"
}

ポイントは以下。

  • 後述するjsdomの設定ファイル.setup.jsを読み込み
  • コンパイラにbabel-registerを設定

jsdomの設定

フルレンダリングを使ったテストでは、shallowレンダリングとは異なりdocumentオブジェクトに対してグローバルにアクセス出来る状態が必要です。enzymeのガイドを参考にjsdomの設定を行います。

enzyme/jsdom.md at master · airbnb/enzyme
https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md

require("babel-register")();

import { jsdom } from "jsdom";

const exposedProperties = ["window", "navigator", "document"];

global.document = jsdom("");
global.window = document.defaultView;
Object.keys(document.defaultView).forEach(property => {
  if (typeof global[property] === "undefined") {
    exposedProperties.push(property);
    global[property] = document.defaultView[property];
  }
});

global.navigator = {
  userAgent: "node.js"
};

windowdocumentなど、ブラウザ固有の値をグローバル変数へ登録しているみたいです。

コンポーネントのテストコードを書く

準備が整ったので、Toastコンポーネントのテストを書いてみます。

import assert from "power-assert";
import sinon from "sinon";
import React from "react";
import { mount } from "enzyme";
import Toast from "../src/Toast";

describe("<Toast />", () => {
  it("Should call onRequestClose callback on close click", () => {
    const props = { onRequestClose: sinon.spy() };
    const wrapper = mount(<Toast {...props} />);

    wrapper.find(".toast__close").simulate("click");
    assert(props.onRequestClose.called === true);
  });

  it("Should call onRequestClose callback on 3 seconds after the mount", () => {
    const clock = sinon.useFakeTimers();
    const props = { onRequestClose: sinon.spy() };
    const wrapper = mount(<Toast {...props} />);

    clock.tick(1000);
    assert(props.onRequestClose.called === false);

    clock.tick(2000);
    assert(props.onRequestClose.called === true);
  });
});

onRequestCloseが呼ばれるかどうか、といった最低限の内容ですがこれで実際にテストを走らせてみます。

$ npm test

  <Toast />
    ✓ Should call onRequestClose callback on close click (43ms)
    ✓ Should call onRequestClose callback on 3 seconds after the mount


  2 passing (84ms)

componentDidMount内で設定したsetTimeoutが正常に動き、マウント後3秒経過時にonRequestCloseが呼び出されていることが確認できました。


週末に公開したreact-md-spinnerでテストを書く時に、enzymeどうやって使おうっていうところを軽く調べた内容について纏めました。enzyme使ってみよう、という方の参考になれば嬉しいです。

Reactに限らずmocha+power-assertを使ったテストが自分の中でよく使うスタックなので、enzymeはそれらと簡単に統合出来て、割と直感的なAPIでテストが書き進められるので良い感じですね。

追記: 2016.07.15

azuさんのツイートで言及いただいていたので、該当箇所を修正しました。恥ずかしながら--requireオプションを使ったことが無かったので参考になりました。ありがとうございます。

一応変更箇所のdiffを貼っておきます。

-    "test": "mocha test/.setup.js test/**/*.spec.js --compilers js:babel-register"
+    "test": "mocha test/**/*.spec.js -r test/.setup --compilers js:babel-register"
Newer Post 24歳になったので抱負とか Older Post Reactで使えるMaterial DesignのSpinnerコンポーネントを作った