前回はTestUtilsの使い方を中心に説明したので、今回はfacebookが開発しているJestというフレームワークとの組み合わせてみたいと思います。
Painless JavaScript Unit Testing
Jestのページには「Painless JavaScript Unit Testing」とある通り導入が簡単という特徴を持っています。
その特徴として「Mock By Default」があって、DefaultでCommonJS Styleのrequire
を全てMockに置き換えます。ちょっと過激な感じですね。
なので、テスト対象の挙動だけに依存したテスト簡単に書くことが出来ます。逆に完全にテスト対象以外はMockになるのでI/FのテストにはならないですがまぁそれはUnit Testの範囲外ということで。
Jasmine
JestはJasmineをベースとしてその上に作られているので基本的なAssertなどはJasmineと同じです。
ただ、Jasmine 1.3をベースとしているので2.0で非同期のテストが書きやすくなった恩恵は受けることができません。
↓それに関するissue
DOM
JestはjsdomによるDOMの上で実行されるので、nodeでのテストのようにcommand line I/Fでテストを実行することが出来ます。
つまりJest
を使っておけばKarmaのようなTest Runnerも使う必要がありません。簡単に導入できます。
Install
Installはjest-cli
をインストールします。
% npm install --save-dev jest-cli
tests
defaultの設定だと、__tests__
というディレクトリを探してきて、その中のファイルをテストとして実行します。
なのでGetting Startedにある通り、__tests__
ディレクトリを作って、その中にテストを置いてjest
を実行するだけでOKです。
globalではなくてdevDependenciesにインストールする場合、package.jsonのscriptsに下記のように書いてnpm test
で実行すると便利です。
"scripts": {
"test": "jest"
},
React.jsのテストをする
ちゃんとDocumentにReact.jsを使ったアプリケーションをテストする場合についても書かれています。
2つの設定をする必要があります。
JSXの変換
JSXを使ってアプリケーションを書いている場合、テストの中でもJSXの変換が必要になります。
そこでjestではscriptPreprocessor
としてpackage.jsonにpreprocessorのscriptを指定することが出来るので、そこでJSXの変換を行います。
これにはreactではなく、react-tools
が必要になるのでinstallしておく必要があります。
"jest": {
"scriptPreprocessor": "preprocessor.js"
},
var ReactTools = require('react-tools');
module.exports = {
process: function(src) {
return ReactTools.transform(src, {harmony: true});
}
};
これだけです。
Mockの解除
上でも書いたようにJestでは全てのrequireがMockを返すようになります。
ただ、React自体をMockされるとテストにならないのでreactへのpathだった場合はMockしないように設定する必要があります。
それもpackage.jsonに指定するだけでOKです。
テストファイルにMockしないファイルを指定出来るのですが、全てのテストでMockしたくない場合はここに書いておくと便利です。
"jest": {
"scriptPreprocessor": "preprocessor.js",
"unmockedModulePathPatterns": ["node_modules/react"]
},
テストを書いてみる
実際にReact Componentのテストを書くとこんな感じになります。
jest.dontMock('../InputArtist');
var React = require('react/addons'),
InputArtist = require('../InputArtist'),
AppTracksActionCreators = require('../../actions/AppTracksActionCreators')
;
describe("inputArtist", function() {
var inputArtist;
beforeEach(function() {
inputArtist = React.addons.TestUtils.renderIntoDocument(<InputArtist />);
});
describe("state", function() {
it("set inputArtist radiohead", function() {
expect(inputArtist.state.inputArtist).toBe("radiohead");
});
});
describe("handleSubmit", function() {
var preventDefault;
beforeEach(function() {
preventDefault = jest.genMockFunction();
inputArtist.setState({ inputArtist: 'travis' });
React.addons.TestUtils.Simulate.submit(inputArtist.getDOMNode(), {
preventDefault: preventDefault
});
});
it ("calls AppTracksActionCreators.fetchByArtist with state.inputArtist", function() {
expect(AppTracksActionCreators.fetchByArtist).toBeCalled();
expect(AppTracksActionCreators.fetchByArtist).toBeCalledWith('travis');
});
it ("calls e.preventDefault", function() {
expect(preventDefault).toBeCalled();
});
});
});
では、詳細を見ていきます。
jest.dontMock('../InputArtist');
MockしたくないmoduleはdontMockで明示的に指定します。
var React = require('react/addons'),
InputArtist = require('../InputArtist'),
AppTracksActionCreators = require('../../actions/AppTracksActionCreators')
;
Reactはpackage.jsonのunmockedModulePathPatternsの指定にマッチしてMockされないようになっていますが、その他のmoduleはMock化されています。
describe("inputArtist", function() {
var inputArtist;
beforeEach(function() {
inputArtist = React.addons.TestUtils.renderIntoDocument(<InputArtist />);
});
describe("state", function() {
it("set inputArtist radiohead", function() {
expect(inputArtist.state.inputArtist).toBe("radiohead");
});
});
この辺りは普通のJasmineのテストコードですね。
React.addons.TestUtils.renderIntoDocument
を使うことでComponentをDOMに紐付けてそれを使ってテストを書いています。
describe("handleSubmit", function() {
var preventDefault;
beforeEach(function() {
preventDefault = jest.genMockFunction();
inputArtist.setState({ inputArtist: 'travis' });
React.addons.TestUtils.Simulate.submit(inputArtist.getDOMNode(), {
preventDefault: preventDefault
});
});
it ("calls AppTracksActionCreators.fetchByArtist with state.inputArtist", function() {
expect(AppTracksActionCreators.fetchByArtist).toBeCalled();
expect(AppTracksActionCreators.fetchByArtist).toBeCalledWith('travis');
});
it ("calls e.preventDefault", function() {
expect(preventDefault).toBeCalled();
});
ここではsubmit buttonが押された場合にfetchByArtist
とe.preventDefault
が呼ばれるかどうかをテストしています。
React.addons.TestUtils.Simulate.submit
を使ってsubmitイベントを発行してイベントオブジェクトのpreventDefaultをjest.getMockFunction
によるMockにすることで呼ばれたことを確認しています。
fetchByArtistは実際だとajaxリクエストが投げられるのですが、JestがMockしてくれているのでとくに意識することなくテストを書くことが出来て簡単ですね。
Mock
Mockはjest.genMockFunction
などのAPIで自分で作ることも出来て、mock
propertyにcalls
やinstances
などの呼び出した情報が記録されていくのでテストではそれを確認します。
また、Mock Functionに対してmockReturnValue
を呼ぶことでMockしながらも指定した値を返すようにしたりすることも可能ですし、mockImplementation
メソッドにcallbackを渡すことで、Mockの実装をすることも出来ます。
Mock Assert
またassertとしてMockを確認するためのものも用意されていて、expect(mockFunc).toBeCalled
のように
することも可能です。
moduleを差し替える
モジュールの実装をテスト時に常に差し替えておきたい場合は、__mocks__
を作りその中にmoduleの実装を置いておくことで可能です。
↓はsuperagentをMockしようとするとエラーになってしまうというissueがあり、それのworkaroundとしてMockを置いています。
Timer
またsetTimerやsetIntervalを使っているような実装に対するテストの場合、jest.runAllTimers
や jest.runOnlyPendingTimers
を使うことで同期的にテストを書くことが出来ます。
runAllTimers
は全てのTimerで待っている処理を実行させて、runOnlyPendingTimers
はその時点でPendingになっているものだけを実行します。
setTimeoutで再帰しているような実装の場合、runAllTimers
を使ってしまうと無限ループになるのでその時はrunOnlyPendingTimers
を使って1つずつ進めながらテストを書いていきます。
API
APIは↓のページにまとまっているので、それぞれは紹介しないですが色々揃ってるのはわかるかと思います。
ちょっとツラいところ
設定で少しはなんとか出来そうですが、Karmaなんかと比べるとテストの実行が遅いのがツラいかもです。
Issueもあるので改善されることを期待しています。
というわけで今回はJestでのテストについて書きました。
明日はFluxについて書きたいと思います。