QUnit-TAP : JavaScript のテスティングフレームワークQUnitからTAP出力する

JavaScript のテスティングフレームワーク QUnit から TAP 出力するための仕組みを作成し、さらに CommonJS 環境下でも動くようにしてみましたので、 github で公開します。ライセンスは QUnit に合わせて MIT と GPLv2 のデュアルライセンスです。


http://github.com/twada/qunit-tap


これは何?

平たく言うと、主に画面非依存の JavaScript コードやサーバサイドで動かす JavaScript コードに対してコマンドラインからユニットテストを行うための仕組みです。


js のユニットテストというとブラウザ上で動かすものが一般的ですが、 DOM に依存しないロジックや抽象的なモジュールのテストはできればコマンドライン上で高速に実行させ、即座にフィードバックを得たいものです。


(更新) ヘッドレスブラウザ PhantomJS が現れたおかげで、 DOM に対するテストも QUnit-TAP でどんどん書けるようになりました


QUnit は、 JavaScript 用のシンプルで強力なテスティングフレームワークです。最初 jQuery のテスト用に開発され、後に jQuery 非依存になり、独立して単体でも使えるようになりました。 QUnit はブラウザが無くても動作するということがリリース当初から謳われており、実際にコマンドラインでも動作しました。しかし、 QUnit はデフォルトではブラウザ向けの出力フォーマットしか提供していませんでした。


そこで、コマンドライン出力の仕組みを開発してみようと考えました。コマンドライン向けの出力のフォーマットとしては、 Perl の世界を中心に広く普及している TAP (Test Anything Protocol) 形式を選択しました。TAP 形式で出力することで、コマンドライン上でテスト結果を確認するだけでなく、 TAP を対象とした既存ツール(例えば prove)との連携を試みました。

CommonJS 対応

加えて CommonJS 対応も行いました。CommonJS とは、パッケージング仕様、依存関係の記述/制御、標準ライブラリなどを仕様化し、サーバサイドでも JavaScript でプログラミングできるようにするための試みです。


テストの仕組みを node.jsnarwhal 上でも使いたい、動作させたいと考え、QUnit-TAP を CommonJS 対応させました。

参考にしたリンク

TAP や Perl の世界のテストについては gihyo.jp の id:charsbar さんの連載がわかりやすいです。


TAP の仕様については以下のリンクを参考にしました


また JavaScript で TAP というと Yappo さんの JSTAPd があります。 JSTAPd は QUnit-TAP よりもさらに包括的なテストを行う仕組みで、 Ajax をモリモリ使った画面のテストでも自動化できることが強みです。興味がある方は是非使ってみてください。

QUnit-TAP はどちらかというと js の単体テストやサーバサイド js (node.js 等)のテストの簡潔な出力を指向しています。


QUnit-TAP の使い方

3ステップで QUnit-TAP を使えるようになります

  1. qunit.js をロード
  2. qunit-tap.js をロード
  3. qunitTap 関数に QUnit オブジェクトと出力用の関数 (print とか puts 等)を渡す

上記3ステップで、 QUnit に TAP 出力する機能を組み込むことができ、テストコードに手を加えなくとも RhinoSpiderMonkey で動かすとテストから TAP 出力ができるようになります。


気軽に QUnit-TAP プロジェクトから lib/qunit-tap.js を自分のところにコピーして使ったり、 git submodule として使ったりしてください。


(更新) node.js のパッケージマネージャである npm にも対応させました。

(2011/03/25 更新) 初期化方法が変わったので記述を修正しました。

サンプルを動かす準備

qunit-tap プロジェクトにサンプルを同梱していますので、それを動かしてみます。


まずは git リポジトリを持ってきて、その後で submodule (svn の external のようなもの) も取得します。

$ git clone git://github.com/twada/qunit-tap.git
$ cd qunit-tap
$ git submodule update --init 


プロジェクトは以下のようなディレクトリ構成になっています。 sample ディレクトリ以下がサンプルコードで、

sample/js
通常の JavaScript で動作させるサンプル
sample/commonjs
CommonJS で動作させるサンプル
sample/interop
通常の JavaScript と CommonJS どちらの環境下でも動作するように試みたサンプル

となっています。

$ tree
.
|-- GPL-LICENSE.txt
|-- MIT-LICENSE.txt
|-- README.md
|-- lib
|   `-- qunit-tap.js
|-- package.json
|-- sample
|   |-- commonjs
|   |   |-- lib
|   |   |   |-- incr.js
|   |   |   `-- math.js
|   |   |-- test
|   |   |   |-- incr_test.js
|   |   |   |-- math_test.js
|   |   |   `-- tap_compliance_test.js
|   |   `-- test_helper.js
|   |-- interop
|   |   |-- index.html
|   |   |-- lib
|   |   |   |-- incr.js
|   |   |   |-- math.js
|   |   |   `-- namespaces.js
|   |   |-- phantomjs_test.sh
|   |   |-- run_qunit.js
|   |   |-- run_tests.js
|   |   |-- test
|   |   |   |-- incr_test.js
|   |   |   |-- math_test.js
|   |   |   `-- tap_compliance_test.js
|   |   `-- test_helper.js
|   `-- js
|       |-- index.html
|       |-- lib
|       |   |-- incr.js
|       |   `-- math.js
|       |-- phantomjs_test.sh
|       |-- run_qunit.js
|       |-- run_tests.js
|       `-- test
|           |-- incr_test.js
|           |-- math_test.js
|           `-- tap_compliance_test.js
|-- test
|   |-- regression.rb
|   `-- regression_spec.rb
`-- vendor
    `-- qunit
        |-- README.md
        |-- package.json
        |-- qunit
        |   |-- qunit.css
        |   `-- qunit.js
        `-- test
            |-- headless.html
            |-- index.html
            |-- logs.html
            |-- logs.js
            |-- same.js
            `-- test.js

16 directories, 43 files
$ 

Rhino で動かしてみる

JavaScript エンジンの Java 実装、 Rhino で動かしてみます。


(Ubuntu 10.04 LTS では sudo aptitude install rhino rhino-doc でインストールできます。 Rhino の実行形式はただの jar ファイルですので、 WindowsMac でも Rhino の jar さえあれば 'java -jar js.jar run_tests.js' で問題なく動作すると思います)

$ cd sample/js/
$ rhino run_tests.js 
# module: math module
# test: add
ok 1
ok 2
ok 3 - passing 3 args
ok 4 - just one arg
ok 5 - no args
not ok 6 - expected: 7 result: 1
not ok 7 - with message, expected: 7 result: 1
ok 8
ok 9 - with message
not ok 10
not ok 11 - with message
# module: incr module
# test: increment
ok 12
ok 13
# module: TAP spec compliance
# test: Diagnostic lines
ok 14 - with
# multiline
# message
not ok 15 - with
# multiline
# message, expected: foo
# bar result: foo
# bar
not ok 16 - with
# multiline
# message, expected: foo
# bar result: foo
# bar
1..16
$ 

実行すると 'ok' や 'not ok' などテスト結果が標準出力に出てきました。この出力形式が TAP フォーマットです。 'ok' で始まる行がアサーション成功、 'not ok' で始まる行がアサーション失敗、 '#' で始まる行はコメントです。


もうすこし詳しく

先ほどコマンドライン上で叩いたのは RhinoSpiderMonkey からテストを動かすために書いたコードです。


sample/js/run_tests.js

load("./lib/math.js");
load("./lib/incr.js");

load("../../vendor/qunit/qunit/qunit.js");
load("../../lib/qunit-tap.js");

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

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

load("./test/math_test.js");
load("./test/incr_test.js");
load("./test/tap_compliance_test.js");
QUnit.start();

単純なコードです。テスト対象をロードし、 QUnitQUnit-TAP をロードし、QUnit-TAP の設定、 QUnit の初期化を行い、テストコードをロードし、最後にテストを開始しています。

プロダクトコード

今回のプロダクトコードのサンプルは単純な足し算とインクリメントのプログラムです。 CommonJS Spec Wiki の CommonJS Modules 仕様のサンプルコードを若干アレンジして使っています。 incr.js が math.js を使う、という関係になっています。


sample/js/lib/math.js

if (typeof math === 'undefined') { math = {}; }

math.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};


sample/js/lib/incr.js

if (typeof incr === 'undefined') { incr = {}; }

incr.increment = function(val) {
    var add = math.add;
    return add(val, 1);
};
テストコードと、簡単な QUnit 入門

テストコードはもちろん QUnit を使っています。 QUnit-TAP は裏手で動きますので、テストコードは純粋な QUnit のコードです。


sample/js/test/math_test.js

module("math module");

test('add' , function() {
         var add = math.add;
         equals(add(1, 4), 5);
         equals(add(-3, 2), -1);
         equals(add(1, 3, 4), 8, 'passing 3 args');
         equals(add(2), 2, 'just one arg');
         equals(add(), 0, 'no args');
     });


sample/js/test/incr_test.js

module("incr module");

test('increment' , function() {
         var inc = incr.increment;
         equals(inc(1), 2);
         equals(inc(-3), -2);
     });
使っている QUnit API の説明


今回は単純なテストコードなので、登場する QUnit API も少量です。以下によく使う QUnit API を列挙します。

test( name, expected, test )
ひとつのテストを定義します。 expected 引数はアサーション実行の予測数で、省略可能です(詳しくは QUnit のテストコードを見てみてください)。
module( name, lifecycle )
複数のテストをまとめます。 module レベルでの初期化も可能です。 なお module 呼び出しは必須ではありません。 test だけでもテストは動作します。
equals( actual, expected, message )
引数 actual と expected が等価であることを == で検証します。message 引数は省略可能です。
ok( state, message )
引数が真であることを検証します(JUnit でいうところの assertTrue に相当します)
same( actual, expected, message )
引数 actual,expected が Array や Object の場合には再帰的に比較を行い、等価であることを検証します (Perl の is_deeply に相当します)
QUnit に新たに追加された(ドキュメント化さていない) API


実は QUnit去年暮れに CommonJS の assert ライブラリに API を合わせていますが、サイトでは特に言及されていないようです。 CommonJS 互換の assertion メソッドが複数追加されています。

equal( actual, expected, message )
equals のエイリアスです。引数 actual と expected が等価であることを == で検証します
notEqual( actual, expected, message)
equal の反対です。引数 actual と expected が等価でないことを != で検証します
deepEqual( a, b, message)
same のエイリアスです。引数 a,b が Array や Object の場合には再帰的に比較を行い、等価であることを検証します (Perl の is_deeply に相当します)
notDeepEqual( a, b, message)
deepEqual の反対です。再帰的に比較し、等価でないことを検証します。
strictEqual( actual, expected, message)
引数 actual と expected が等価であることを === で検証します
notStrictEqual( actual, expected, message)
strictEqual の反対です。引数 actual と expected が等価でないことを !== で検証します


QUnit には他にも非同期テストの仕組みやカスタマイズなどまだまだ機能はありますが、さらに詳しくは QUnit のサイトを参考にしてください。



テスト出力仕様の重要性

TAP を選択した理由は、テスト結果の出力形式がシンプルで、かつ特定のプログラミング言語やテスティングフレームワークから独立しているからです。


TAP の出自が Perl コミュニティなのでツールは当然 Perl が多いですが、本質的には UNIX 伝統のプレーンテキスト文化に則っています。つまり、テスト結果を UNIX 文化であるパイプとフィルタを使ったプログラミングの入力として使えるということです。プログラマが自分のためのツールを書きやすいフォーマットだと感じます。


失敗したテストがあるかどうか

例えば、失敗したときだけ分かるようにしたいならば grep すれば良いわけです

$ js run_tests.js | grep '^not ok'
prove を使ってみる

TAP を対象としたテスト収集/自動化ツール prove も使えます。
この例は単純ですが、テストの数が多くなってくると prove の便利さが実感できます。

$ prove --timer --exec=/usr/bin/rhino run_tests.js 
[12:28:04] run_tests.js .. ok     1037 ms
[12:28:05]
All tests successful.
Files=1, Tests=7,  1 wallclock secs ( 0.05 usr  0.00 sys +  1.27 cusr  0.11 csys =  1.43 CPU)
Result: PASS
$ 
ファイルが更新されたらテストを動かす

ファイルが更新されたらテストを動かす autotest のようなしくみも簡単に作れます。


以下のスクリプトは私が使っているスクリプト(を、ちょっとサンプル用に編集したもの)です。 inotifywait コマンドでファイル更新を監視し、更新があったらテストを実行、テスト結果を notify-send (Macgrowl のようなもの) に通知しています。


autotest.sh

#!/bin/sh

while inotifywait -r -e modify public/test ; do
    js tests.js | tee js_test.log | grep '^not ok' > /dev/null
    if [ $? -eq 0 ]; then
        notify-send -u critical -t 1000 --icon=./fail.png 'FAILED'
    else
        notify-send -u normal -t 1000 --icon=./pass.png 'SUCCEEDED'
    fi
done

ソフトウェアを梃子(てこ)として使う

UNIX という考え方』の中に、「ソフトウェアを梃子(てこ)として使う」という考え方が出てきます。機能満載の大きなソフトウェアを書くのではなく、それぞれが単機能で価値に集中したプログラムをシェルスクリプトやグルー言語でつなげて仕事を成し遂げるという考え方です。


今回私が書いたコードは QUnit に TAP 出力をさせてみるというコード、行数は現時点で 71 行です(そのほとんどは CommonJS 対応です)。たったそれだけのコードでも、出来ることがいろいろと増えてきました。「ソフトウェアを梃子(てこ)として使う」ということが実感できるのはこういうときです。


UNIX という考え方』は、私のバイブルです。UNIX の背後に流れる価値観、設計思想、シンプルな設計とは何かを学べる、多くの技術者におすすめの本です。

UNIXという考え方―その設計思想と哲学

UNIXという考え方―その設計思想と哲学



さて、 CommonJS についても書きたいことがいろいろあるのですが、エントリが長くなってきたのでまずここで終わりにします。 CommonJS についてや、 CommonJS と普通の js のどちらでも動くコードの模索についても、今後どこかで書いてみたいです。


このエントリを読んで QUnit-TAP に興味を持たれた方は、是非一度試してみてください。