愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

Test::QUnit - mozrepl経由でコマンドラインからJavaScriptのテストを実行する

マクラ - JavaScriptのテストについて

テストのないコードはコードではなく、テストを書かないプログラマプログラマではなく、テスティングフレームワークのない言語は言語と呼ぶに値しない。と以上のような偉そうなことを言う資格は全くないし狂信的でもない僕ですが、少なくともまともに動くコードであることを証明するために、人並みにはテストを書きます。

それでまあ、最近JavaScriptばかり書いてるのですが、JavaScriptのテスティングフレームワークって大体以下のようなものに分かれると思っています。

  1. ブラウザ上で動かすことを前提としたもの(JsUnit, QUnitなど)
  2. RhinoやSpiderMonkeyなど、ブラウザから独立したJavaScriptエンジンで実行することを前提としたもの(JsUnit, QUnit-TAPなど)
  3. 2. に加え、env.js(http://www.envjs.com/)などを利用してサーバサイドJavaScriptエンジン上でブラウザをエミュレートし、ローカルでクライアントサイドJSのテストも実現可能にしたもの(Jasmineなど)


ちなみにJsUnitが二個ありますが、実はJsUnitと名のつくJS用のテスティングフレームワークって二つあって、割と有名でHudson用のプラグインがあったりするのが1.のJsUnit(http://www.jsunit.net/)で、マイナーというかそもそもShindigのJSのテスト用に使われてるのしか見たことないにも関わらずmavenのpluginがあったりするのが2.のJsUnit(http://jsunit.berlios.de/)です。


んでそれぞれを比較していくと、まず1. はクライアントサイドJSをテストする上で最も素直な方法なのですが、いかんせんテストのためにブラウザで特定のURLを叩かなければいけないのが面倒です。テストというのはコマンド一発で叩けるか、あるいはIDEからポチっとな、でまとめて実行できないと著しくやる気の下がるものです。

次に2.ですが、これは環境非依存なJS、つまりクライアントサイドであろうとサーバサイドであろうと関係なく動く類のライブラリ的なものであれば問題なくテストできます。しかしちょっとでもクライアントサイドJS特有のもの(windowとか)がテストの上で必要になってくるとアウトで、自分で頑張ってエミュレートしたり、あるいはクライアントサイドが絡んでくるものだけ別のテスティングフレームワークに頼る(!)ことになってしまいます。

個人的に今後の可能性があるのが3. かなあと思っているのですが、Jasmine試したことないので分かりません。そのうち試したいです。この手法をとるテスティングフレームワークの利便性はつまるところ、ブラウザのエミュレーションの精度次第だと思います。クライアントサイドJSの技術が進み、そこでのブラウザ内部のエミューレションが進んだ暁にはベストな選択肢に成り得るのではないかなあ、と勝手に思っています。(Jasmineについてはあんまり分かっていないのでツッコミ歓迎です)

mozreplとゆかいな仲間たち

ちょっと話が変わりますが、Firefoxにはmozrepl(http://wiki.github.com/bard/mozrepl/)というアドオンがあって、これをFirefoxにインストールした上で動作させると、起動しているFirefoxにport 4242でtelnet接続できるようになります。接続した後は対話的シェルでFirefoxの中を探検したり、 任意のJavaScriptコードを実行して遊ぶことができます。ちなみにmozreplのシェルは扱いにくいので、

rlwrap telnet localhost 4242

のようにrlwrapでラップしてあげると使いやすくなります。

mozreplを使えるとJSのデバッグなど中々役に立つのですが、やはりFirefoxの中身を本格的にいじくりまわしたい!となると対話的シェルでは限界があります。プログラムからmozreplを操作できるといいのになあ、というかそういうライブラリってないの?と思って検索してみたらありました。

MozRepl - http://search.cpan.org/~zigorou/MozRepl/

なんだか、どこかで見たことのある人がAuthorですね!


これを使うとPerl経由で以下のようにJavaScriptのコードが実行できます。

use MozRepl;

my $repl = MozRepl->new;
$repl->setup;

$repl->execute("alert('Fuck You!')");

なんだかとっても変態的な楽しい気分になってきましたね。


これだけでも結構色々なことが出来そうですが、MozReplを元にさらに変態的な便利な使い方が出来るようにしたのがMozRepl::RemoteObject(http://search.cpan.org/dist/MozRepl-RemoteObject/lib/MozRepl/RemoteObject.pm)です。

これを使うと、JavaScriptの文を評価して、その結果のJSオブジェクトを等価なPerlのオブジェクト(無論そのまま変換されているわけではない)としてかなり透過的に扱うことが可能になります。等価と透過をかけているわけではないです。例えば、以下のような簡単なコードでJSオブジェクトのプロパティを表示できます。

use MozRepl;
use MozRepl::RemoteObject;

my $repl = MozRepl->new;
my $bridge = MozRepl::RemoteObject->install_bridge($repl);

my $jsobj = $bridge->expr(<<'JS');
    (function getPoint() {
        return { x: 200, y: 100 };
    })();
JS

print $jsobj->{x}; # => 200
print $jsobj->{y}; # => 100

MozRepl::RemoteObjectを使うと、cpanのSYNOPSYSにもあるように$tab->{linkedBrowser}->{contentWindow}->{document}->{body}のようにJSのオブジェクトをPerlから手繰っていったり、PerlからJSの関数を$tab->{linkedBrowser}->{contentWindow}->alert();のように実行できたりして面白いので、是非試してみてください。

Test::QUnit - Yet Another QUnit Testing Framework

以上のことを踏まえて、

  1. QUnitで書かれたテストを
  2. コマンドライン上から一発で(Perlの場合はproveですね)
  3. かつ本物のブラウザ上で動くので細かいことは気にせず

実行できるテスティングフレームワークフレームワークと呼べるほど大した機能ないけどなんとなくカッコいいので)を作ってみました。現在はgithub上にのみ置いてあります。

http://github.com/zentooo/p5-test-qunit


書き方はこんなカンジです。

use strict;
use warnings;

use Test::More;
use Test::QUnit;

inject_select_window_function("function(window) {
    return window.parent !== window;
}");

qunit_ok('http://path/to/qunit/test.html', 'qunit sample');

done_testing;

基本的にはqunit_ok({テストしたいQUnitテストのURL}, {テストの説明}); するだけです。これを実行すると、 当該URLのQUnitテストの結果がTAP形式に変換された上で吐き出されます。内部的には、{テストの説明}を第一引数としたsubtestになっています。なので新しめのTest::Moreが必須です。

以上のようなテストをt/以下に置いた上でproveしてやると、QUnitテストがガガガガっと実行されます。なお、トップレベルではない(iframeとか)ウィンドウ上でQUnitテストを実行している場合もあるかと思い、inject_select_window_functionという関数に「windowオブジェクトを引数に取りテストを実行したいwindowならtrue, そうでない場合falseを返すような無名関数」を投げると指定したwindowでテストが出来るようにしてあります。上の例だと、topレベルでないwindowを選択しています。画面上にiframeが一つだけあり、そこでQUnitのテストを実行する場合はこれでじゅうぶんです。


内部では、以下のようなことをしています。

  • addEventListenerでwindowのloadイベント発火時にQUnitオブジェクトのlogメソッド, doneメソッドを書き換えるように指定。logメソッドは1テスト実行ごとに呼ばれ、doneメソッドはテスト全てが終了した時に呼ばれる
  • logメソッド内部では受け取った「テストの成否」と「テストメッセージ」を含むJSオブジェクトを配列に突っ込む
  • doneメソッド内部では終了フラグを立てる
  • QUnitテストを実行し、終了フラグが立つまで待ってからMozRepl::RemoteObject経由でテスト結果の配列を取り出し、TAP形式に変換して出力。そして後始末。


QUnitにlogやdoneなどフック用の関数が予め用意してあったからこそ出来たことだけど、他のテスティングフレームワークに関しても同じような関数さえあれば理論上同じことができるはず。

現在はデフォルトで上記のMozReplとMozRepl::RemoteObjectを利用したTest::QUnit::Bridge::MozReplを使うようになっているけど、ここは一応Test::QUnit::Bridgeを継承して3つくらいのメソッドをオーバライドしたクラスを作れば実装が取り替えられるようになっている。試していないのでそこの切り替え部分がちゃんと動くかは未確定。

chromerepl(http://github.com/kzys/chromerepl)経由でChromeでもテストするためのBridgeに関してはそのうち気が向いたらプロセス間通信の練習がてらつくるかもしれないけど、IEとかは、まあ、誰かが作ってくれるとみんな幸せになれるかもしれないですね。丸投げですね。

Test::QUnitの「QUnitのlogをmozreplで乗っとればいいんじゃね?」というメインアイディアはid:ZIGOROuさんに頂きました。感謝感謝。

実装的にはまだまだイケてない点が多いですが、JSのテストで困っている人は使ってみると多少幸せになれるかもしれません。Enjoy testing!