JavaScript - QUnitでBDD風に書いたりCIするために調べたこと
調べたこと
- QUnit
- テスト関数を入れ子にしたい
- pavlov / specit(BDD風にテストを階層化)
- sinonjs(モックライブラリ)
- phantomjs(コマンドライン実行)
- travis連携の仕方
QUnit
基本的な使い方
- インストール
- 非UIテスト(純粋なロジックテスト)
- UI(DOM)テスト
- 非同期テスト
あたりはググればいっぱい情報出てくるので割愛。
サンプルコードは一応リポジトリにはある。
テスト関数を入れ子にしたい
QUnitで一番困るというか、好みじゃない点です。
例えばこういうプロダクトコードがあるとします。
var Calc = (function(){ function Calc() { } Calc.prototype.add = function(x, y) { return x + y; } Calc.prototype.sum = function() { var sum = 0; for (i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } return Calc; })();
対するテストコードはこんな感じでいいでしょう。
module('Calc', { calc: new Calc() }); test('add - 2つの数を足す', function() { deepEqual(this.calc.add(2, 1), 3); deepEqual(this.calc.add(0, 0), 0); deepEqual(this.calc.add(-1, 2), 1); }); test('sum - 引数をすべて足す', function() { deepEqual(this.calc.sum(), 0); deepEqual(this.calc.sum(1), 1); deepEqual(this.calc.sum(1, 2, 3, 4), 10); });
今calc.sum
は可変長引数だけを想定していますが、
配列も受け取れるようにしたい、と思ったとします。
テストコードに追加します。
module('Calc', { calc: new Calc() }); test('add - 2つの数を足す', function() { deepEqual(this.calc.add(2, 1), 3); deepEqual(this.calc.add(0, 0), 0); deepEqual(this.calc.add(-1, 2), 1); }); test('sum - 引数をすべて足す', function() { deepEqual(this.calc.sum(), 0); deepEqual(this.calc.sum(1), 1); deepEqual(this.calc.sum(1, 2, 3, 4), 10); }); test('sum - 配列の場合はその合計値を出す', function() { deepEqual(this.calc.sum([]), 0); deepEqual(this.calc.sum([1]), 1); deepEqual(this.calc.sum([1, 2, 3, 4]), 10); });
このように、どの関数も同じ階層で書く必要があります。 いやです。階層化したいです。
test
関数にtest
関数を入れることでそれっぽくはできます。
ただしその場合、外側のtest
にも何かしらassertを書かなくてはいけません。
またmodule
で設定したcalc
も参照できません。
(例)
test('sum', function(){ var calc = new Calc(); // moduleで設定したcalcが見えないので test('引数をすべて足す', function(assert) { deepEqual(calc.sum(), 0); deepEqual(calc.sum(1), 1); deepEqual(calc.sum(1, 2, 3, 4), 10); }); test('配列の場合はその合計値を出す', function() { deepEqual(calc.sum([]), 0); deepEqual(calc.sum([1]), 1); deepEqual(calc.sum([1, 2, 3, 4]), 10); }); ok(true); // 何かassertが必要 })
QUnitプラグインを使うことで、これが多少改善されます。
pavlov / specit(BDD風にテストを階層化)
Pluginsにいくつかプラグインがありますが、 そのうち次の2つはQUnitをBDD風に書けるようにするものです。
使い方は簡単で、普通にscriptタグで読み込むだけです。
(例)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>QUnit Sample</title> <link rel="stylesheet" href="../vendor/qunit/qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="../vendor/qunit/qunit.js"></script> <script src="../vendor/qunit/pavlov/pavlov.js"></script> <!-- <-これ --> <script src="../main/calc.js"></script> <script src="./calc_test.js"></script> </body> </html>
これを使えば階層化したい問題は解消されます。
どちらも素晴らしいですが、別の問題がありました。
specit
jQueryに依存している
全然致命的というほどではないんですが、なんでjQuery必須なの...。
(jQueryのユーティリティ関数使っているからなんですけど)
親describeで定義した変数が見えない
順を追って見てみます。 まずはプラグインなしで書いた場合。
module('Calc', { calc: new Calc() }); test('add - 2つの数を足す', function() { deepEqual(this.calc.add(2, 1), 3); }); test('sum - 引数をすべて足す', function() { deepEqual(this.calc.sum(1, 2, 3, 4), 10); }); test('sum - 配列の場合はその合計値を出す', function() { deepEqual(this.calc.sum([1, 2, 3, 4]), 10); });
分かる。
続いてspecitで書いた場合(describeの入れ子はなし)
describe('Calc', function(){ var calc; before(function() { calc = new Calc(); }); it('add - 2つの数を足す', function() { // あくまでプラグインなので、assertとかでQUnit本来の関数が使える // specitに付いてくるxSpec的な書き方でももちろんOK deepEqual(calc.add(2, 1), 3); }); it('sum - 引数をすべて足す', function() { deepEqual(calc.sum(1, 2, 3, 4), 10); }); it('sum - 配列の場合はその合計値を出す', function() { deepEqual(calc.sum([1, 2, 3, 4]), 10); }); });
分かる。
続いて本題。sumを入れ子にする。
describe('Calc', function(){ var calc; before(function() { calc = new Calc(); }); it('add - 2つの数を足す', function() { deepEqual(calc.add(2, 1), 3); }); describe('sum', function(){ it('引数をすべて足す', function() { deepEqual(calc.sum(1, 2, 3, 4), 10); }); it('配列の場合はその合計値を出す', function() { deepEqual(calc.sum([1, 2, 3, 4]), 10); }); }); });
実行してみる。
TypeError: Cannot call method 'sum' of undefined
分からない。悲しい。
pavlov
describeがglobalじゃない
なのでspecitみたいにいきなりdescribeを書き始められない。
pavlov.specify('Calc', function(){ // ホントはここに'Calc'と書きたいけど外側に書いちゃったし... describe('_', function(){ var calc; before(function(){ calc = new Calc(); }); describe('add', function(){ it('2つの数を足す', function() { // pavlovも独自assert拡張あるし、QUnit標準assertも使えるよ! deepEqual(calc.add(2, 1), 3); }); }); describe('sum', function(){ it('引数をすべて足す', function() { deepEqual(calc.sum(1, 2, 3, 4), 10); }); it('配列の場合はその合計値を出す', function() { deepEqual(calc.sum([1, 2, 3, 4]), 10); }); }); }); });
でもpavlovはdescribeが入れ子になっていても変数が参照できます。素敵。
あと不満がもう一つ。
エラー箇所のファイル名が出ないんですが
Test failed: _, sum: わざと失敗するよ Failed assertion: asserting 0 is same as 100, expected: 100, but was: 0 at file:///Users/kanno/workspace/qunit-example/vendor/qunit/qunit.js:593 at file:///Users/kanno/workspace/qunit-example/vendor/qunit/pavlov/pavlov.js:727 at file:///Users/kanno/workspace/qunit-example/vendor/qunit/pavlov/pavlov.js:230 at :30 at file:///Users/kanno/workspace/qunit-example/vendor/qunit/qunit.js:203 at file:///Users/kanno/workspace/qunit-example/vendor/qunit/qunit.js:361 at process (file:///Users/kanno/workspace/qunit-example/vendor/qunit/qunit.js:1453) at file:///Users/kanno/workspace/qunit-example/vendor/qunit/qunit.js:479 Took 3068ms to run 19 tests. 18 passed, 1 failed.
「at :30」ってどういうこと。
pavlovがいい感じなのだけど、上記2点だけ解消したい。
まだ調べきれていないので、解決策が用意されているかもしれない。
pavlovをもし使う場合の補足
given
を使うと、パラメタライズドテストも書けます。
こんな感じ。(READMEより)
given([5, 4], [8, 2], [9, 1]). it("should award a spare if all knocked down on 2nd roll", function(roll1, roll2) { // this spec is called 3 times, with each of the 3 sets of given()'s // parameters applied to it as arguments if(roll1 + roll2 == 10) { bowling.display('Spare!'); } assert(bowling.displayMessage).equals('Spare!'); });
ところでパラメタライズドテストってなんですか?
sinonjs(モックライブラリ)
普通に入れて普通に使えた。
pavlovと組み合わせても問題なく使えました。
PhantomJS(コマンドライン実行)
インストールとかはググればいいとして。
PhantomJSでQUnitを実行するためのランナーは2種類あります。
- PhantomJS付属のもの
- {インストールディレクトリ}/share/phantomjs/examples/run-qunit.js
- QUnit Addon
QUnit Addonの方が出力が分かりやすいので、そっちがいいと思います。
実行はphantomjs {上記runnerjs} {テスト対象html}
です。
例えばphantomjs vendor/qunit/phantomjs/runner.js test/index.html
です。
travis連携の仕方
travisでは何もせずともphantomjsが使えます。
なので、.travis.yml
に以下を書くだけでOKです。
script: phantomjs vendor/qunit/phantomjs/runner.js test/index.html
簡単ですね。travisすごい。
TODO(優先度高い)
- 毎回htmlファイル作ったり、テストファイルごとにscriptタグの追加メンドイ
- どうするのが良いのだろう
TODO(優先度低い)
- Jenkinsで走らせる
- Jenkins未使用だし、travis使っているから今のとこ不要
- 自動監視による再テスト
- Guardを使うのが楽なのかな
- Jasmineならguard-jasmineというのがあるっぽいんだけどなあ
- Vimで実行
- quickrun(watchdogsの方がいいかな?)
- このあたりは実際にテスト書き始めたら設定する
これから
初期段階のいくつかの要素からQUnitにしようと思っていたけど、
Jasmineももうちょっと調べてから判断しようかなあ。
ホントはmochaが一番良さそうなんだけど、残念ながら対象がnodeアプリじゃない。