読者です 読者をやめる 読者になる 読者になる

Aqutras Members' Blog

株式会社アキュトラスのメンバーが、技術情報などを楽しく書いています。

Reactのコンポーネントをヘッドレスブラウザでテストする

こんにちは。maxmellon です。

今回は,Reactで作成したコンポーネントの単体テストを行う方法を紹介したいと思います.

本記事は,Reactの基本的な仕様を理解した上で 話を進めていきます。 Reactの基本的なことにつきましては 下記などを参照してください.

https://facebook.github.io/react/docs/why-react.html http://mizchi.hatenablog.com/entry/2014/09/02/201728

はじめに

フロントエンドのテストできていますか?

viewのテストはAPIなどと比べて優先度が低く扱われているように感じています.

理由としては,

  • ユーザーの入力順序による状態の変化
  • APIなどと比べ,変更頻度が高い

などが上がると思います. 従来のような,いわいる紙をwebページに置き換えたようなアプリケーションであれば, チェックシートのようなもので対応できていましたが,ここ最近のJavaScriptでかかれた アプリケーションは,1ページあたりの操作が非常におおくなり,複雑化している傾向に有ると思います これらをチェックシートなどで,対応するのは相当コストが高いです.

そこで,テストコードによる自動化です. Reactは,幸いなことにComponent単位でのテストが想定されて設計されています.

( 本項目は次の記事に強く影響されています : http://qiita.com/teppei_tosa/items/46087a35776e14c89d42 )

テスト構成

  • ava : テスティングフレームワーク & アサーション
  • ava-spec : BDD helper avaにシナリオなどを導入する
  • enzyme : component test utils 今回の主役
  • jsdom : ヘッドレスブラウザ, nodeにwindowを提供するもの
  • sinon : stub, spy, mock などを提供

これらのツールを用いて,Reactのコンポーネントのテストを記述していきます.

mochaではなく,avaを使う理由

avaはmochaとは異なり,テストケースごとにプロセスが異なり複数のテストケースを 並列で実行してくれます.そのためテストの実行がはやいので採用しました. また,avaは polyfill なしで async/await や generator function を使うことができます

これにより,Promiseを始めとした非同期処理のテストを美しく描き上げることができます

テスト環境を構築する

jsdomは,nodeのバージョンが4系統である必要があります. ここでは,現行最新である node v6.2.2 を使うことにします. また,このnodeをnvmを用いてインストールします. macを前提に,環境構築手順を簡単に載せておきます.

今後のサポート期間を考えて,nodeは4系統か,6系統を用いましょう.5系統は年内で サポートが終了します.

// node のインストールは,テスト環境以前に開発でも必要ですが,jsdomが指定のバージョン以降である必要があるため載せておきます.

プロジェクトのディレクトリ構成
.
├── package.json
├── index.js
├── index.html
├── .babelrc
├── src
│    └── components
└── test
     └── components

src/components 以下に reactのcomponents を置きます. test/components 以下に reactのcomponentsのテストコード を置きます.

nvm の インストール
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
$ exec $SHELL

これで,nodeのバージョンを切り替えるためのCLI, nvm のインストールが完了しました. 次に,nvm を通して 特定のバージョンの node をインストールします.

$ nvm install v6.2.2
$ nvm alias default v6.2.2
$ nvm use v6.2.2

インストールして,デフォルトのnodeのバージョンを 6.2.2 にしています.

テストに必要なものもろもろインストール

React, ReactDOM, babel 周りはインストール済みとする.

$ npm install --save-dev ava ava-spec jsdom sinon enzyme react-addons-test-utils

react-addons-test-utils の バージョンは必ずReactにあわせてください. enzymeは,projectにあったReactのバージョンで動かすようになっているため, enzyme自体の依存には,react, react-dom, react-addons-test-utils は含まれていません.

そのため,プロジェクトにあった,react, react-dom, react-addons-test-utils を インストールしてください.

テストを実行するためにもろもろ設定する

テストを実行するために,もろもろ必要な設定があります. 大体Reactであればほとんど同じ設定でいけます.

jsdomでwindowを用意する

jsdomを使ってglobal.window を 用意します

test/setup.js

const { jsdom } = require('jsdom');

global.document = jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
console.debug = console.log; // for ReactDOM
/* eslint max-len: 0 */
global.navigator = {
  userAgent: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2454.85 Safari/537.36',
};

console.debug が存在せず,react-dom がこけるので代わりに,console.log を入れています. 参考 : https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md#using-enzyme-with-jsdom

avaの設定をpackage.jsonに記述する

package.json

  ・・・

  "ava": {
    "require": [
      "babel-register",
      "./test/setup.js"
    ],
    "files": [
      "test/**/*.js",
      "!test/setup.js"
    ],
    "babel": "inherit"
  },

  ・・・

ava を実行するときに読み込むJavaScriptのテストコード群と,実行時に必要なファイルを requireします babel-registerをrequireし,ECMAScript2015 で テストコードを記述できるようにします.

そして,先ほど作成した,./test/setup.js をここで require し,テスト時に必要な window を用意します

これで,

$ ava

を実行することで,テストコードを実行する準備ができました. では,さっそく,テストを記述していきましょう

ava-spec で シナリオベースのテストコードを記述してavaでテストを実行する

Component の テストコードを書く前に,ava, ava-specに利用方法に触れていきたいと思います.

基本的には,次のように書きます.ava-specを用いることで, mocha, jasmineの ように記述することができます

describe('AVA Spec tutorial',  it => {
  it('sample test', t => {
    t.deepEqual([1, 2], [1, 2]);
  });
});

存在するアサーションは結構異なりますが,ruby でいう rspec のようにテスト書いていくことができます. avaは power-assert を組み込みで持っているので,mochaのようにいちいち外部からアサーションを持ってこなくても イケてるアサーションを使うことができます.もちろん,外部のアサーションライブラリを扱うことができます.

ava標準のアサーション一覧は次を参照してください https://github.com/avajs/ava#assertions

enzymeで始めるComponentテスト

テストコードの書き方を説明するために,適当にテスト対象の適当なComponentを用意します.

src/components/SampleComponent.js

import { Component, PropTypes } from 'react';

export default class SampleComponent extends Component {
  static get propTypes() {
    return {
      handleClick: PropTypes.func,
    };
  }

  constructor(props) {
    super(props);
    this.state = {
      text: '',
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.props.handleClick();
    this.setState({ text: 'clicked!' });
  }

  componentDidMount() {
    console.log('component mounted');
  }

  render() {
    return (
      <div className="sample-component">
        <span>サンプルコンポーネント</span>
        <button
          className="sample-button"
          onClick={this.handleClick}
        />
      </div>
    );
  }
}

上のようなコンポーネントをテストコードによってテストしていきたいと思います

まず,enzyme をつかって, jsdom で用意した global.windowに,Component を レンダリングします.

enzyme は React Component の幾つかのレンダリングの方法を提供してくれます

  • enzyme.shallow : その名の通り浅いレンダリング,ルートコンポーネントのみレンダリング
  • enzyme.mount : shallowとは対象てきに子コンポーネントすべてをレンダリング
  • enzyme.render : ReactComponent を 生DOMに変換してレンダリング,ReactDOM.render で コンポーネントをレンダリングするのと同等

それぞれ,サンプルを書いていきたいと思います

import React from 'react';
import { shallow, mount, render } from 'enzyme';
import sinon from 'sinon';
import SampleComponent from '../../src/component/SampleComponent';

describe('<SampleComponent />', it => {
  // 基本的なテストの例
  it('expect className of SampleComponent is sample-component', test => {
    // SampleComponent を シャローレンダリングする
    const wrapper = shallow(<SampleComponent />);
    // wrapper に インスタンスが入ってるかどうか確認
    test.truthy(wrapper);
    // div タグがどうか
    test.is(wrapper.node.type, 'div');
    // props である className が正しいかどうか
    test.is(wrapper.node.props.className, 'sample-component');
  });

  // クリックイベントのテスト例
  it('expect called handleClick when click of button', test => {
    // sinonをつかってダミー関数を用意
    const handleClickDummy = sinon.spy();
    // SampleComponent を マウントする
    const wrapper = mount(<SampleComponent handleClick={handleClickDummy} />);
    test.truthy(wrapper);
    // デフォルトのstateが入っているかを確認する
    test.is(wrapper.state().text, '');
    // wrapper から button コンポーネントのインスタンスの取得
    const button = wrapper.findWhere(node => node.props().className === 'sample-button');
    // クリックをシミュレート
    button.simulate('click');
    // クリックに紐付いたイベントが呼ばれたかどうかを確かめる
    test.is(handleClickDummy.callCount, 1);
    // setState されたかどうかを確かめる
    test.is(wrapper.state().text, 'clicked!');
  });

  // ライフサイクルのテスト例
  it('expect called componentDidMount when mounted component', test => {
    // componentDidMount を 監視
    sinon.spy(SampleComponent.prototype, 'componentDidMount');
    // マウント
    const wrapper = mount(<SampleComponent />);
    // マウント後,componentDidMountが呼ばれたかどうかテスト
    test.is(Foo.prototype.componentDidMount.callCount, 1);
    // 監視の開放
    SampleComponent.prototype.componentDidMount.restore();
  });

  it('rendered the SampleComponent', test => {
    const wrapper = render(<SampleComponent />);
    // レンダリングされたコンポーネントに含まれてる文字列を確かめる
    test.is(wrapper.text(), 'サンプルコンポーネント');
  });
});

ライフサイクルをテストするには,すこし工夫が必要ですが,Reactでつくられたコンポーネントを そこそこ直感的に書くことができます.

enzyme が提供しているAPIが非常は非常に多いので,一般的なコンポーネントに対するテストをコードで書いて, よく使いそうなものに絞って紹介しました.

airbnb/enzyme の 詳細は https://github.com/airbnb/enzyme/tree/master/docs

テストを実行する

./node_modules/.bin/ava を実行することで,テストが走ります. ただこれだと,毎回入力するのがめんどくさいので,npm script を利用します.

package.json

  ・・・
  "scripts": {
    "test": "ava",
  },
  ・・・

npm script は 自動的に実行ファイルの対象に node_modules/.bin 以下を追加してくれます なので,省略してかけます.

testに追加した script は,次のように実行することができます.

$ npm test
$ npm t  # 省略できる

test は例外的に run を省略でき,その他のscriptはrunサブコマンドを通して実行するので注意してください.

$ npm run hoge # package.json の scripts の hoge を実行

カバレッジを測定する

この記事のavaを使うという項目が非常に参考になります

http://qiita.com/59naga/items/7db57c88ce8cca560ea9

$ npm install --save-dev nyc

上記コマンドでnycをインストールして package.json に次のscripts を記述

package.json

  "scripts": {
    "test": "ava",
    "cover": "nyc --reporter=lcov --reporter=text npm run test"
  },

そして,次のコマンドを実行

$ npm run cover

これでカバレッジを測定することができます

まとめ

  • ava + enzyme + sinon + jsdom といった環境で ReactComponent をテストする
  • enzyme つかうと,facebook/react-addons-test-utils with jest より書きやすいし,自由に環境選べる
  • ava は はやい(体感でも)
  • nyc で カバレッジ測って テスト欲を上げる.

ヘッドレスブラウザを使っているので,CIもしやすいです.phantomjsより圧倒的に早いのも評価できるところだとおもいます.