JavaScript - QUnitでBDD風に書いたりCIするために調べたこと

調べたこと

  • QUnit
    • テスト関数を入れ子にしたい
  • pavlov / specit(BDD風にテストを階層化)
  • sinonjs(モックライブラリ)
  • phantomjs(コマンドライン実行)
  • travis連携の仕方

kannokanno/qunit-example

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(モックライブラリ)

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アプリじゃない。