簡易テストツールを公開しておきます

今年はブログを書きたいと述べて二ヶ月以上経ってしまいました。

最近、大規模だったり高速だったりという需要に応えるJavaScriptを書く道具がいくつも出てきています。一方で、小規模でもっと手軽にという道具がイマイチ少ない気がします(主観)。jQueryでもうちょっと届かないところを埋めるそういう道具を少しずつ準備していこうかなと思います。

まずは昔作ったユニットテストを改良したので公開しておきます。

utestは最小限のユニットテストを提供します。昔作ったのでIE5.5でも動きますが、そういう目的にはあんまり使うつもりは無いです。

方針としては容易に書けるテストを目指しました。自分はテスト書くのがたるくて手が止まるというのがありがちなので……。

例えば、以下に示すsample.jsをテストすることを考えます。

var sample = {
  valid: true, // trueであるかテストしたい
  empty: [], // nullではなく空の配列であることをテストしたい
  ok: function () {
    return 1; // ok()を実行したら1が返り、ng()は実行できないことをテストしたい
  },
  done: false, // delayを実行後、遅れてtrueになるかテストしたい
  delay: function () {
    setTimeout(function () { sample.done = true; }, 0);
  },
  // 画像読み込みをテストしたい
  image: function (src, fn, err) {
    var img = new Image;
    img.src = src;
    img.onload = fn;
    img.onerror = err;
    return img;
  }
};

QUnitでは以下のように書くと思います。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="../qunit/qunit.css">
<script src="../qunit/qunit.js"></script>
<script src="sample.js"></script>
<script src="test.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">test markup</div>
</body>
</html>

test.js

test("true test", function() {
  expect(1);
  ok(sample.valid);
});

test("function test", function() {
  expect(2);
  same(sample.ok(), 1);
  raises(function () { sample.ng(); });
});

test("array test", function() {
  expect(2);
  equal(QUnit.equiv(sample.empty, []), true);
  equal(QUnit.equiv(sample.empty, null), false);
});

test("async test", function() {
  expect(2);
  sample.delay();
  ok(!sample.status);
  stop();
  setTimeout(function() {
    ok(sample.status);
    start();
  }, 1000);
});

testAsync("image test", function() {
  expect(1);
  sample.image('sample.png', function () {
    ok(true);
    start();
  }, function () {
    ok(false);
    start();
  });
});

testAsync("image test (2)", function() {
  expect(1);
  sample.image('notfound.png', function () {
    ok(false);
    start();
  }, function () {
    ok(true);
    start();
  });
});

非同期テストがもう少し簡単に書ければ嬉しいな、と感じます。うまく書くやり方を知っている方は教えてください。

一方でutestは以下のように書きます。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Utest Test Suite</title>
<script src="utest.js"></script>
</head>
<body>
<script src="test.js"></script>
</body>
</html>

test.js

utest("true test", [ sample.valid ]);

utest("array test", [
  [ sample.empty, [] ],
  [ sample.empty, '!==' null ],
]);

utest("function test", [
  function () { return [ sample.ok(), 1 ]; },
  utest.raise(function () { sample.ng(); })
]);

utest("async test", [
  function(test) {
    sample.delay();
    test(!sample.status);
    test(sample.status, 1000);
  }
]);

utest("image test", [
  function(test) {
    var ok_test = test(),
        ng_test = test();
    sample.image('sample.png', function () {
      ok_test(true);
    }, function () {
      ok_test(false);
    });
    sample.image('notfound.png', function () {
      ng_test(false);
    }, function () {
      ng_test(true);
    });
  }
]);

全体的にズボラな感じに仕上がっています。QUnitはよくできているのですが、もう少し手軽で気楽に書ければと思って作ってみました。今までテスト書かずに書き捨てていたコードにも少しはテスト書くようになりたいです。

まとめ

  • utestはQUnitを用いるのもちょっとかったるい時に使う手軽な道具
  • 世間のちゃんとしたユニットテストの代替じゃなくて、ユニットテスト使わずに書いていた(自分の)ところ向け
  • IE5.5で動く
    • けど、もうそういう古いの向けのコードは今後は書かないよ

記法の備忘録

まとめのあとに続けます。

上記のutestの記法は一番省略を効かせています。上記の例とは異なりますが、省略を行わずに書いた例を示します。

utest("sample", function (regist) {
  //  :
  // setup
  //  :
  regist([
    "sync1", function (test) {
      var test1 = test();
      test1(tmp.flag);
    },
    "sync2", function (test) {
      var test1 = test(), test2 = test();
      test1(tmp.bool);
      test2([ tmp.num, '==', '1' ]);
    },
    "async", function (test) {
      var test1 = test(), test2 = test();
      setTimeout(function () {
        test1(tmp.delay_bool);
        setTimeout(function () { test2([ tmp.delay_num, '==', '1' ]); }, 2000);
      }, 1000);
    }
  ], function () {
    //  :
    // teardown
    //  :
  });
});

setupとteardownが不要な場合があると思います。その場合、returnで書くこともできます。

utest("sample", function (regist) {
  return [ ... ]; // regist([ ... ]);
});

また、テストごとに関数で囲むのであれば、全体の関数も不要かもしれません。

utest("sample", [
  "sync1", function () { ... },
  "sync2", function () { ... },
  "async", function () { ... }
]);

さらにテスト名はもっとルーズに書ける方が良いでしょう。

// 関数に名前をつけた例
utest("sample", [
  function sync1() { ... },
  function sync2() { ... },
  function async() { ... }
]);
// オブジェクトを使った例
utest("sample", {
  sync1: function () { ... },
  sync2: function () { ... },
  async: function () { ... }
});

このように配列やオブジェクトで記述できます。なお、さらにルーズにテスト名を書かずに連番による自動命名に任せることもできます。

sync1は同期するテストですが、test()は関数を生成し、一次変数に格納しています。

function sync1(test) {
  var test1 = test();
  test1(tmp.flag);
}

これは以下のように書けます。

function sync1(test) {
  test()(tmp.flag);
}

一つ目のカッコが鬱陶しいのと、テストなら第一引数にbooleanかarrayかobjectしか入らないので、その場合は、カッコを省略できるようにしました(それ以外が入る場合があれば、自動命名が働くので注意する必要があります)。

function sync1(test) {
  test(tmp.flag);
}

テストセットの登録と同じようにtestの数が一つならreturnで返せるようにします。

function sync1() {
  return tmp.flag;
}

ものによっては関数スコープも不要でしょう。

tmp.flag

sync2も同期テストです。なお、二つ目のテストはreturnで置換可能です。

function sync2(test) {
  test(tmp.bool);
  return [ tmp.num, '==', '1' ]; // == test([ tmp.num, '==', '1' ]);
}

両方returnで記述することもできます。配列を使わないのはテストで配列を利用するためです。

function sync2() {
  return {
    bool_test: tmp.bool,
    num_test: [ tmp.num, '==', '1' ]
  };
}

名前も単なる連番でも構いません。

function sync2() {
  return {
    0: tmp.bool,
    1: [ tmp.num, '==', '1' ]
  };
}

自分で名前をつけるのが億劫なら引数に与えたものを上記の連番で名前をつけたオブジェクトに変換する関数もあります。

function sync2() {
  return utest.and(tmp.bool, [ tmp.num, '==', '1' ]);
}

この形になれば関数スコープもいりませんね:-)

utest.and(tmp.bool, [ tmp.num, '==', '1' ]);

非同期テストを見ていきます。

function async(test) {
  var test1 = test(), test2 = test();
  setTimeout(function () {
    test1(tmp.delay_bool);
    setTimeout(function () { test2([ tmp.delay_num, '==', '1' ]); }, 2000);
  }, 1000);
}

このテストも一見すると、以下のように書けるかもしれません。ですが、setTimeoutの関数が実行されなかった場合、テストがいくつあるかわからないため、実行漏れが起きてしまいます。そのため、非同期テストの場合はtest()でテスト実行関数の生成を必ず行う必要があります。

function async(test) {
  // 全部でいくつのテストをするの?
  setTimeout(function () {
    test(tmp.delay_bool);
    setTimeout(function () { test([ tmp.delay_num, '==', '1' ]); }, 2000);
  }, 1000);
}

ですが、setTimeoutを組み合わせてテストを書くことは多々ありそうです。そこでテスト関数の第二引数に遅延時間、第三引数に終了後実行関数を設定できるようにしました。

function async(test) {
  var test1 = test(), test2 = test();
  test1([ tmp.delay_bool ], 1000, function () {
    test2([ tmp.delay_num, '==', '1' ], 2000);
  });
}

ここでテストの意味は変わってしまいますが、このtest2はtest1が終わって2秒後ではなく、テスト開始からtest1の遅延を含めた3秒後に開始と読み替えます。すると、以下のように書けます。

function async(test) {
  var test1 = test(), test2 = test();
  test1(tmp.delay_bool, 1000);
  test2([ tmp.delay_num, '==', '1' ], 3000);
}

これなら、テストの初回の関数取得を省略して、こう書けます。

function async(test) {
  test(tmp.delay_bool, 1000);
  test([ tmp.delay_num, '==', '1' ], 3000);
}

そんなわけで、短くすると(色々副作用がある場合があるので場合によりけりですが)こう書けます。

utest("sample", [
  sync1: tmp.flag,
  sync2: utest.and(tmp.bool, [ tmp.num, '==', '1' ]),
  async: function (test) {
    test(tmp.delay_bool, 1000);
    test([ tmp.delay_num, '==', '1' ], 3000);
  }
]);

以上のように簡潔にサッとテストを書き残すことができます。