satococoa's blog

Web や iOS アプリを作るエンジニアの日記です

QUnit + QUnit-TAPでJSのTDDをしてみた

JavaScriptでTDDするにはどうすればいいのかな・・・と思い、TDDBCではQUnit + QUnit-TAPが使われていたようなので、とりあえずその環境を整えてみました。

普段rspecを使っている自分としてはrspecっぽく書けるJasmineも気になっているのですが、とりあえずはQUnitを。

インストール

QUnit-TAPはnpmで提供されているので、それで入れるのが簡単そう。globalに入れることもできますが、とりあえずカレントディレクトリのnode_module/に入る以下のコマンドでインストール。

 $ npm install qunit-tap

設定

まずは設定とか必要なモジュールのrequire等を行うtest_helperを作ります。

QUnit-TAPにサンプルでついてくるtest_helper.jsをほぼそのまま、パスのあたりだけ調節して使っています。

exports = module.exports = global;

var tryRequireThese = function() {
  var args = Array.prototype.slice.apply(arguments);
  for(var i=0; i < args.length; i+=1) {
    try {
      return require(args[i]);
    } catch(e) {
      // ignore
    }
  }
  throw new Error("cannot find moduele: " + args);
};

QUnit = require('qunit-tap/vendor/qunit/qunit/qunit.js').QUnit;
var qunitTap = require('qunit-tap').qunitTap;

var sys = tryRequireThese("sys", "system");
for (var i in sys) exports[i] = sys[i];
puts = (typeof sys.puts === 'function') ? sys.puts : sys.print;

qunitTap(QUnit, puts, {noPlan: true});

QUnit.init();
QUnit.config.updateRate = 0;

exports.assert = QUnit;

テストを書いてみる

このへん利点がよくわかっていないのですが、TAPという形式での出力はproveというperl製のコマンドを使って実行するのが便利らしいので、それに従っておこうと思います。

すると、t/というディレクトリ以下にテストファイルを置いておくとproveコマンドが自動的にそのディレクトリ以下のファイルを自動的にテストファイルとして扱ってくれるらしいので、従います。

t/array_test.js

require('../test_helper.js');

QUnit.module('Module Array');
QUnit.test('#length', function() {
  var ary = new Array(5);
  assert.equal(ary.length, 5);
});

QUnit.test('#push', function() {
  var ary = new Array();
  ary.push('first');
  assert.equal(ary.length, 1);

  ary.push(2);
  assert.equal(ary.length, 2);
});

QUnit.test('#concat', function() {
  var ary1 = [1, 2, 3];
  var ary2 = [4, 5, 6];
  var res = ary1.concat(ary2);
  // assert.equal(res, [1, 2, 3, 4, 5, 6]); # => fail
  assert.deepEqual(res, [1, 2, 3, 4, 5, 6]);
});

QUnit.start();

実行してみます。

$ prove -vc --exec=node --ext=.js
t/array_test.js .. 
# module: Module Array
# test: #length
ok 1
# test: #push
ok 2
ok 3
# test: #concat
ok 4
1..4
ok
All tests successful.
Files=1, Tests=4,  0 wallclock secs ( 0.04 usr  0.00 sys +  0.12 cusr  0.01 csys =  0.17 CPU)
Result: PASS

proveコマンドの-vは結果の表示を詳細に、-cは色を付けて、--execはテストコードの実行コマンド、--extはテストコードの拡張子ですね。

ちょっとハマったのが、配列同士をassert.equal()で比較するとfalseになってしまうこと。確かにnodeコマンドのコンソールで[1,2] == [1,2]と打ってもfalseが返ってきます。このへん、JavaScriptの動作としては配列の中身までは比較せずに、同じオブジェクトへの参照を持っているかどうか、ということを比較しているからfalseなのでしょうか。assert.deepEqualを使うことで意図通りのテストができました。

せっかくなのでもう一つテストファイルを足してみます。

t/string_test.js

require('../test_helper.js');

QUnit.module('Module String');
QUnit.test('#match', function() {
  var str = 'test';
  assert.ok(str.match(/te.{2}/));
});

QUnit.test('#split', function() {
  var str = 't,e,s,t';
  assert.deepEqual(str.split(','), ['t', 'e', 's', 't']);
});

QUnit.start();

Greenでした。OKですね。

では、独自に作ったJSをテストすることにします。
テストファーストということで、テストファイルから。

t/calculator_test.js

require('../test_helper.js');
var cal = require('../lib/calculator.js').Calculator;

QUnit.module('Module Calculator');
QUnit.test('#initialize', function() {
  assert.ok(cal);
});

QUnit.start();

この時点でテストを実行すると当然Red。
実装しましょう。

lib/calculator.js

var Calculator = {
};

exports.Calculator = Calculator;

JSのモジュールとか、やったことは無いのですがこんなやり方でいいのかな?とにかくGreenになったので、テストコードを足して失敗になったことを確認した後、実装します。

t/calculator_test.js

QUnit.test('#sum', function() {
  assert.equal(10, cal.sum(5, 5));
  assert.equal(15, cal.sum(5, 5, 5));
});

lib/calculator.js

var Calculator = {
  sum: function() {
    var result = 0;
    for (var i = 0, l = arguments.length; i < l; i++) {
      result += arguments[i];
    }
    return result;
  }
};

GreenになったのでOKのようですね。

自動でテストを実行

やっぱりテストファイルを更新したら自動的に更新されて欲しいですよね。
とりあえずrubyですがguardを使ってみました。

$ gem install guard guard-shell growl rb-fsevent
$ guard init shell

生成されたGuardfileを以下のように書き換えて、lib/, t/以下のファイルが更新されたら自動的にproveコマンドを実行するようにしました。

guard 'shell' do
  watch(%r{lib|t/(.+)\.js}) {|m| `prove -vc --ext=.js --exec=node` }
end

結果、JSとperlrubyを駆使した(?)面白い環境になりました。
色がつかないのでなんとかしたいのですが、どうすればいいのかよくわからなかったです。

まとめ

今回はnode.jsを使って、簡単なモジュールのテストをしてみました。

一口にJSのテストと言っても、サーバーサイドのJSをテストしたいのか、ブラウザ上のJS(特にDOM操作が入ったりするもの)をテストしたいのか、JS含めたアプリケーションの動き全体をテストしたいのか、そのへんを見極めておかないといったい自分が何をしたいのかを見失ってしまいそうで、要注意ですね。