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.js や narwhal 上でも使いたい、動作させたいと考え、QUnit-TAP を CommonJS 対応させました。
参考にしたリンク
TAP や Perl の世界のテストについては gihyo.jp の id:charsbar さんの連載がわかりやすいです。
- 第27回 Test::Most:Test::Moreでは物足りなくなってきたら
- 第28回 Test::More:no_planからdone_testingへ
- 第29回 Test::Base:データ本位のテストをするときは
TAP の仕様については以下のリンクを参考にしました
- http://search.cpan.org/~petdance/Test-Harness-2.65_02/lib/Test/Harness/TAP.pod
- http://testanything.org/
- http://en.wikipedia.org/wiki/Test_Anything_Protocol
また JavaScript で TAP というと Yappo さんの JSTAPd があります。 JSTAPd は QUnit-TAP よりもさらに包括的なテストを行う仕組みで、 Ajax をモリモリ使った画面のテストでも自動化できることが強みです。興味がある方は是非使ってみてください。
QUnit-TAP はどちらかというと js の単体テストやサーバサイド js (node.js 等)のテストの簡潔な出力を指向しています。
QUnit-TAP の使い方
3ステップで QUnit-TAP を使えるようになります
上記3ステップで、 QUnit に TAP 出力する機能を組み込むことができ、テストコードに手を加えなくとも Rhino や SpiderMonkey で動かすとテストから 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 ファイルですので、 Windows や Mac でも 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' で始まる行がアサーション失敗、 '#' で始まる行はコメントです。
もうすこし詳しく
先ほどコマンドライン上で叩いたのは Rhino や SpiderMonkey からテストを動かすために書いたコードです。
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();
単純なコードです。テスト対象をロードし、 QUnit と QUnit-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 に相当します)
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 文化であるパイプとフィルタを使ったプログラミングの入力として使えるということです。プログラマが自分のためのツールを書きやすいフォーマットだと感じます。
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 (Mac の growl のようなもの) に通知しています。
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 の背後に流れる価値観、設計思想、シンプルな設計とは何かを学べる、多くの技術者におすすめの本です。

- 作者: Mike Gancarz,芳尾桂
- 出版社/メーカー: オーム社
- 発売日: 2001/02/01
- メディア: 単行本
- 購入: 40人 クリック: 498回
- この商品を含むブログ (145件) を見る
さて、 CommonJS についても書きたいことがいろいろあるのですが、エントリが長くなってきたのでまずここで終わりにします。 CommonJS についてや、 CommonJS と普通の js のどちらでも動くコードの模索についても、今後どこかで書いてみたいです。
このエントリを読んで QUnit-TAP に興味を持たれた方は、是非一度試してみてください。