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より圧倒的に早いのも評価できるところだとおもいます.