目的
ReactのコンポーネントってUnitTestしにくいですよね。
まずjsxで書いてるから変換が必要だし、DOM環境を用意して配置する必要があるし、renderメソッドの中で他のコンポーネントを呼んでると、そっちの挙動も気にしなきゃいけないし。。。
その面倒くさいのをなんとかするテストフレームワークとしてJestがあるんですが、そっちはそっちでハマりどころが多いし。。。(Windows環境でのセットアップがやたら大変、node0.12で動かない、jasmineベース、マルチブラウザテストできない、jsdomのバージョンが古い、実行が遅い etc etc...)
というわけで、Jestでやりたかったことができるテスト環境を0から考えてみました。
ゴール
- reactのコンポーネントの単体テストができる。
- 非同期(Promise)のテストができる。
- 依存しているコンポーネント・モジュールの中身をmockに差し替えられる。
- jsx(&es6)で書かれたコンポーネント・テストコードでもテストできる。
- 他のテストツールと連携できる。
- ソースコードを汚さない。独特な記法をしない。
- 設定がややこしくない。「読めないけどなぜか動く」とかにしない。
解決法の概要
- mochaベースにする。
- テスト対象を
vm.runInNewContext
で読み込むことで、テスト対象のrequireの中身を動的に差し替える。 - テストの前にjsx->jsの変換を済ませてしまう。
- 相対パスがややこしかったので、node_module以下にsrcへのシンボリックリンクを張る。
ソースコードは以下に置いています。
https://github.com/uryyyyyyy/React_mocha_boilerplate/tree/my_mocker
仕組みについてはコード読めばわかると思うので特に説明はしません。
使い方例を以下に書きます。
使い方
試してみる
https://github.com/uryyyyyyy/React_mocha_boilerplate/tree/my_mocker
をダウンロードして、
npm install -g gulp
npm install
ln -s ../src node_modules/
をしたあとに、
gulp test:testUtil --path src/reactComponents/_tests_/nestComponentTest.js
あたりを試してみてください。
mockテストをしたい場合
var assert = require('assert')
var testMocker = require('src/testMocker.js')
describe("#mockTest", function () {
var mock={};
mock["src/functions/AsyncUtil.js"] = {
hello: function hello() {
console.log("hello mock");
}
};
var util2 = testMocker.loadModule("./testSandbox/src/functions/util2.js", mock);
it("mock hello", function () {
util2.hello();
});
});
src/functions/util2.jsのhello関数のテストをしたいとします。
しかし、hello関数はAsyncUtil.jsのメソッドに依存していて、そのままでは単体テストができません。(AsyncUtil.jsのテストをしたいわけではないのです。)
そこで、事前にAsyncUtil.jsのmockオブジェクトを作っておいて、testMocker経由でutil2.jsを読み込みます。すると、util2.hello()を実行すると、mockオブジェクトのメソッドが呼ばれていることがわかります。
Reactのコンポーネントのテストをしたい場合
テストコード例:
var assert = require('assert')
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var testMocker = require('src/testMocker.js')
var jsdom = require("jsdom");
global.document = jsdom.jsdom("<!doctype html><html><body></body></html>");
global.window = document.parentWindow;
global.navigator = window.navigator;
describe("#react nestComponent test", function () {
var mock={};
var NestComponent = testMocker.loadModule("./testSandbox/src/reactComponents/NestComponent.js", mock);
it("render NestComponent, but don't render untouchableOne", function () {
var nestComponent = TestUtils.renderIntoDocument(
<NestComponent />
);
console.log(nestComponent.calc());
assert.equal(nestComponent.calc(), 3);
});
});
src/reactComponents/NestComponent.jsのcalc関数のテストをしたいとします。
しかし、reactのコンポーネントは一度DOM上に設置(instance化)しないとテストできません。
そこで、まずはjsdomを用意してあげます。
また、DOM上に設置する際にrenderが呼ばれるので、そのままではUntouchableOne
という呼び出したくないコンポーネントが呼ばれてしまいます。
そこで、testMockerはデフォルトでReactコンポーネントのダミーに差し替えるようにしています。(UntouchableOneのrenderメソッドの中にあるconsole出力が呼ばれてないことで確認できます。)
これらよって、reactのコンポーネントでも普通のjsオブジェクトと同じようにテストすることができます。
厄介だったところ
- 相対パス問題
→ルートディレクトリでgulp-mochaを使ってテストコードを呼ぶと、requireとかの相対パスがズレてしまいます。
そこで、シンボリックリンクを貼ってみたり部分的に相対パスを使うことでどうにか解消しています。
もっと良い解決法があったら教えて下さい。。。
-
jsdomのバージョン
→僕の環境では4.0.0では動きませんでした。 -
babelの扱い
→jsx→jsへの変換を事前にやっておいてsandbox上に置くのはスマートじゃないんですが、テストコードをbabel変換しつつ呼び出してvm.runInNewContext
の中でもbabelを呼ぶと、「二回呼ぶなよ」ってbabelから怒られてしまいました。
無理やり解決することもできるかもですが、仕組みがややこしくなるので断念しました。