jestを使ってReact.jsなスクリプトをテストする
公式ドキュメント: https://facebook.github.io/jest/docs/tutorial-react.html
参考: http://qiita.com/koba04/items/2f97904b3ca247fc1917
参考資料1: https://facebook.github.io/react/docs/test-utils.html
参考資料2: https://facebook.github.io/jest/docs/mock-functions.html
React.jsで書かれたコンポーネントをjestを使ってテストしてみる。ただそれだけ
環境設定
jest自体がnode0.8系か0.10系でしか動かない模様(node0.12系でやるとSEGVする)のでnodeのバージョンは0.10系を使う
で例のごとくpackage.jsonを設定する
{
"name": "sample",
"version": "0.0.1",
"main": "js/app.js",
"dependencies": {
"react": "^0.13.1",
"react-router": "^0.13.2",
"react-tools": "^0.13.1"
},
"devDependencies": {
"browserify": "^8.0.3",
"envify": "^3.0.0",
"jest-cli": "^0.4.0",
"reactify": "^0.15.2",
"watchify": "^2.6.2"
},
"scripts": {
"start": "watchify -o bundle.js -v -d js/app.js",
"build": "browserify . -t [envify --NODE_ENV production] -o bundle.js",
"test": "jest"
},
"browserify": {
"transform": [
"reactify",
"envify"
]
},
"jest": {
"scriptPreprocessor": "<rootDir>/preprocessor.js",
"unmockedModulePathPatterns": [
"node_modules/react"
]
}
}
devDependenciesにてjest-cliの依存性を追加して、testでjestが動く似用に設定。んでjestの設定部分ではscriptProcessorで指定したpreprocessor.jsを使って、jestがテストを行う際にJSX構文が認識出来るように事前にコンパイルするような形で提供してテストを実行出来るようにしておかないといけない模様。っていう事で以下をpreprocessor.jsとして作成しておく
var ReactTools = require('react-tools');
module.exports = {
process: function(src) {
return ReactTools.transform(src);
}
};
公式ドキュメントにも同様に書いてたはずなのでそのまま使う。ちなみにbabel.jsを使ってES6で書いてるようなケースの場合は普通にscriptProcessorにbabel-nodeを指定すれば良い模様(詳しくは上記参考のkoba04さんが書いてるreact-boilerplateを参照)
でunmockedModulePathPatternsはjestでテストする際にあたって読み込まれるスクリプトがまるごとモック化される模様。React.jsをモック化されても困るのでそういうのを除外する対象とするのがこの設定っぽい(自動でそういうふうにされないようにするにはjest.autoMockOffなりをテスト上でやればイケるっぽいらしい)
でこれからReactを使ったアプリケーションを作っていく
js/app.js
var React = require("react"),
Echo = require("./components/Echo.js");
React.render(<Echo />, document.body);
んまぁここは今回関係無いので省略
js/components/Echo.js
// AppActionCreators.sayは引数をtoUpperCaseされるだけのメソッドが定義されたオブジェクト
var React = require("react/addons"),
AppActionCreators = require("../actions/AppActionCreators");
var Echo = React.createClass({
displayName: "echo",
getInitialState: function() {
return { msg: "hoge" };
},
handleChange: function(value) {
this.setState({ msg: value });
},
handleClick: function() {
var msg = AppActionCreators.say(this.state.msg);
this.setState({ msg: msg });
},
render: function() {
var inputLink = {
value: this.state.msg,
requestChange: this.handleChange
};
return (
<div>
<span>{this.state.msg}</span><p />
<input type="text" valueLink={inputLink} />
<button onClick={this.handleClick}>click</button>
</div>
);
}
});
module.exports = Echo;
っていう感じで
- デフォルトのstate.msgはhoge
- <input type=“text">でvalueLinkを設定してchangeイベントをfire出来るようにする
- <button>でAppActionCreators.sayした結果をmsgに突っ込む
的な要件なだけなのだけど、これが意図した動作になるかどうかをテストする
js/components/_tests_/Echo-test.js
"use strict";
jest.dontMock("../Echo");
var React = require("react/addons"),
TestUtils = React.addons.TestUtils;
describe("Echo", function() {
it("call render", function() {
var Echo = require("../Echo");
var echo = TestUtils.renderIntoDocument(<Echo />);
var span = TestUtils.findRenderedDOMComponentWithTag(echo, "span");
expect(span.getDOMNode().textContent).toEqual("hoge");
});
it("change event test", function() {
var Echo = require("../Echo");
var echo = TestUtils.renderIntoDocument(<Echo />);
// 初期化(getInitialState)でツッコまれたstate.msgがhogeであるかを検証
expect(echo.state.msg).toEqual("hoge");
var input = TestUtils.findRenderedDOMComponentWithTag(echo, "input");
TestUtils.Simulate.change(input, {
target: { value: "fuga" }
});
// <input>にデータを設定してchangeイベントをfireさせる (TestUtils.Simulate.changeにて)
expect(echo.state.msg).toEqual("fuga");
});
// やってる事は上記と同様だけど、こっちはrequestChangeを使ってchangeイベントをfireさせる
it("change event test (valueLink.requestChange)", function() {
var Echo = require("../Echo");
var echo = TestUtils.renderIntoDocument(<Echo />);
expect(echo.state.msg).toEqual("hoge");
var input = TestUtils.findRenderedDOMComponentWithTag(echo, "input");
input.props.valueLink.requestChange('fuga');
expect(echo.state.msg).toEqual("fuga");
});
it("button click event test", function() {
var AppActionCreators = require("../../actions/AppActionCreators");
// AppActionCreators.sayが呼ばれるとAを返す
AppActionCreators.say.mockReturnValue("A");
var Echo = require("../Echo");
var echo = TestUtils.renderIntoDocument(<Echo />);
expect(echo.state.msg).toEqual("hoge");
var input = TestUtils.findRenderedDOMComponentWithTag(echo, "input");
input.props.valueLink.requestChange('fuga');
expect(echo.state.msg).toEqual("fuga");
var button = TestUtils.findRenderedDOMComponentWithTag(echo, "button");
// clickイベントをfire
TestUtils.Simulate.click(button.getDOMNode());
// モックされたAppActionCreators.sayが呼ばれたかを検証
expect(AppActionCreators.say).toBeCalled();
// 上記でAppActionCreators.say.mockReturnValue("A")しているので一致しないのでfailする
expect(echo.state.msg).toEqual("FUGA");
});
// shallow renderingを使ってみただけなので以下は説明省略
it("shallow rendering", function() {
var Echo = require("../Echo");
var renderer = TestUtils.createRenderer();
renderer.render(<Echo />);
var result = renderer.getRenderOutput();
expect(result.type).toBe("div");
var children = result.props.children;
var child1 = children[0];
var child2 = children[1];
var child3 = children[2];
var child4 = children[3];
expect(child1.type).toEqual("span");
expect(child1.props.children).toEqual("hoge");
expect(child2.type).toEqual("p");
expect(child3.type).toEqual("input");
expect(child4.type).toEqual("button");
});
});
button click event testだけはテストがfailする。<input>にchange eventでfireしているのにも関わらず<button>をクリックした場合に発生するAppActionCreators.sayがモックされてAしか返さないようになっているので
んまぁjasmineベースなテストを書く事で出来る。ただ上記でも書いてるけど特定していないスクリプト等のオブジェクトは全てモック化されるので呼び出されるメソッド等に適切にreturnValue等を返すか、特定の処理をするようにする(mockImplementationとか)ような感じな事しなければならない(現在モック化されたメソッドのオリジナルをコールする方法は判明してない)
結構長ったらしく書いたけどこれでもReact.js+jestでのテストのほんの一部にしかすぎないので色々今後も使って分かった事とか色々書くと思う
余談
ほう > モジュールの実装をテスト時に常に差し替えておきたい場合は、__mocks__を作りその中にmoduleの実装を置いておくことで可能
— kinjouj (@kinjou__j) 2015, 4月 10
っていう事らしい。詳しくはやってみてから書く