読者です 読者をやめる 読者になる 読者になる

Block Rockin’ Codes

back with another one of those block rockin' codes

QUnitの基本的な使い方

JavaScript jQuery QUnit

[追記] 2013/9/1
三年前の記事が未だに読まれているようなので、一応書いておきますが、あれから色々変わってもっと良いものも出ています。 QUnit でも別に問題はないですが、今から QUnit を使うよりは http://visionmedia.github.io/mocha/:title=mocha] とかの方が個人的にはお勧めです。とにかく、今は色々あるのでもっと別の選択肢調べて見ることを個人的にはおすすめします。別に QUnit は使わないほうが良いとは言いません。




JavaScriptのテスティングフレームワークはいろいろありますが、自分は今主にQUnitを使っているので、少し使い方をまとめて見たいと思います。

[追記]今回作成したソースを上げました。ninja.js

QUnit とは

QUnitはもともと、jQueryをテストするために開発されたJavaScript Unit Testing Frameworkです。
しかし現在ではQUnitjQueryへの依存がなくなっているそうです。
つまりjQueryがないプログラムにも使えますし、テスト実行のためにjQueryを入れる必要もありません。
一般的なJavaScriptプログラム、そしてサーバサイドJavaScriptでも強力で手軽なテスティングフレームワークとして使用されています。

QUnitの導入方法

QUnitの導入はとても簡単です。以下のリポジトリから一式を取得し、任意の場所に配置するだけです。取得したQUnitのディレクトリ構造は以下のような形になっています。



リポジトリ: jquery/qunit · GitHub


QUnit の本体はqunit/quint.jsです。QUnitは、JavaScriptで実行したテスト結果をブラウザで確認するスタイルなので、結果を表示するためtest/index.htmlとqunit/quint.cssが同梱されています。


testディレクトリは、作成したテストを配置するディレクトリです(もちろんパスが合ってれば好きに配置できます)。現在のバージョンには、テストのサンプルとしてtest.js, same.js が同梱されています。


とりあえず今の構造のままindex.html を開いて見ましょう。緑のバーがいくつか表示されると思います。これが、test.jsとsample.jsのテスト結果です。このテストは、QUnit自体をテストするテストになっているようです。


では、自分のテストを作成して見ましょう。今回は、js/ninja.js をテストしたいと思います。
まずテスト対象ninja.jsを、jsディレクトリに配置します。
次に、 ninja.jsのテストを書く、test-ninja.jsをtestディレクトリに配置します。


配置したら、各ファイルをtest/index.htmlに読み込ませます。


読み込む順番は、


(0,jQuery)
1,QUnit(qunit.js)
2,テスト対象(ninja.js)
3,テスト(test-ninja.js)


というような感じです。
ついでに、以降メッセージに日本語を使う場合は、以下を加えておくことをお勧めします。

<meta charset="UTF-8">


ホスティングを使うと、こんな感じ。


<script src="http://code.jquery.com/jquery-latest.js"></script>
<link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen" />
<script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>


以降では、サンプルのためにjQueryを読み込んでおり、
Ajax用のスタブファイルとしてjs/text.txt (中身は"moved\n"のみ)を用意しています。

こんな感じ。



基本的なテストの書き方

まず一番単純なテストを書いて見ましょう。テストはtest()メソッド内に記述します。

test("テスト名", function(){ /*テスト*/ });


テスト名は、index.htmlでの表示に使用さます。
テスト本体は、無名関数内に記述します。


評価用のメソッドは以下のようなものがあります。

  • ok(状態, メッセージ)
  • equals(実際値, 期待値, メッセージ)
  • same(実際値, 期待値, メッセージ)


メッセージはいずれも省略可能です。
これも index.htmlで表示に使用されるので、そのつもりで書きましょう。


equals()は内部的には、== の評価、
same()は === での評価になります。
違いを意識し、適切に使うようにしましょう。(安易なequalsは怪我の元です)


実際の例はこんな感じです。

test("normal test",function(){
	ok(true, "trueならGreen");
	equals(1, "1", "1=='1'");
	same(1, 1, "1===1");
});

実行結果はこんな感じ。




非同期テストの書き方


JavaScriptにおいて当たり前のようにAjaxが使われることも多いので、
QUnitは非同期な処理をテストする仕組みも用意されています。


例を元に見ていきます。


まずなにか非同期な処理をするninja.warp()メソッドがあったとします。
結果は"moved"が期待されるとしたら、こんなテストが書けます。

test("syncTest",function(){
    ninja.warp();
	var ans = ninja.ans;
    same("moved\n", ans);
});


しかし、これではテストは成功しません。
なぜなら、warpは非同期なので、warpが実行されたら、
処理が二つに別れて、結果が戻る前にsameが評価されてしまいます。
おそらくundefinedとなるでしょう。

stop()とstart()の利用


期待しているテストをするためには、
warpが"moved"を返すまでsameの評価を待たなくてはなりません。
その為に、QUnitはstop()とstart()という二つのメソッドを提供しています。


それを使うと以下のように記述できます。

test("stop() and start()",function(){
    ninja.warp(); // 非同期メソッドを実行
    stop(); // 停止
    setTimeout(function(){
        start(); // 10ms後再会
		var ans = ninja.ans; 
		same(ans, "moved\n"); // 評価
     },10);
});


まず、warp()を実行します。そして直後にstop()を呼んでいます。
stop()はその名の通り、テストの実行を停止するので、
以降は評価されません。テストを再開するためには、start()を呼ぶ必要があります。
そこで、 setTimeoutを使用して、start()を呼ぶタイミングを遅らせます。
上の例では10msテストを止めていることになるので、その間にansには"moved"が代入され、
再開後のsameはgreenになります。
もちろん時間がかかる処理はtimeoutする時間を伸ばす必要があります。
わかってしまえば、どうという仕組みではないですね。


また、QUnitにはasyncTestというメソッドが用意されています。
これは、ただ単にstop()を自動で挟んでくれるラッパーようです。
先ほどのを aysncTestを用いて書き直すと以下のようになります。

asyncTest("asyncTest",function(){
    ninja.warp(); // 非同期メソッドを実行
//    stop(); // 停止
    setTimeout(function(){
        start(); // 10ms後再会
		var ans = ninja.ans; 
		same(ans, "moved\n"); // 評価
     },10);
});

いずれのテストも timeoutの時間は手動決打ちなので、
本当は呼んだメソッドのコールバックでstart()させられればいいのですが、
もとの実装と切り離す為には、これが一番現実的なようです。
工夫できる点としては、非同期なのでメソッドをバラでasyncTestするより、
一つのasyncTestにまとめて最後の方に一気にやるのがいいかもしれません。
あとはテストの可読性との兼ね合いでしょうか。

stop(ms)の使い方

また、stop()には引数で。時間を渡すことができます。
これはなんらかの理由でstartが実行されないと、
stop()したままになってしまう為、タイムアウトを実現する為の引数です。
タイムアウトすると、該当するテストを落として、以降のテストを再開してくれます。


次の例は、dummy.txtに問い合わせをしています。
しかし、dummy.txtは存在しないので、もしstop()で停止すると、
処理が戻らなくなります。
stop(100)にすると、100ms待っても処理が戻らない場合、
処理を再開(実装はok(false)を実行してから、start()を実行)します。

test("stop(ms)",function(){
    // stop(); // これだと処理が戻ってこない
	stop(100); // 100msでテストを強制再開
    $.get("dummy.txt", function(data){ // この処理が戻ってこないと、start()で再開されない。
		start();
		same(data, "moved\n"); // 強制再開された場合は、これは評価されない。
 	});
	ok(true, "これは影響を受けない");
});

expected の使い方

test,asyncTest には、expectedオプションがあります。
また独立したexpected()というメソッドもあります。
これはそのテストのなかで、予想されるテストの成功数を記述すると、
その数に合わない時にRedにするという機能です。
多くても、少なくてもダメです。


例えば、

といいつつ、非同期では簡単に再現できる例が思いつかなかったので、
ちょっとひねった例です。

ninja.jsにこんなメソッドが定義されていたとします。

var ninja = {
	set : function(){},
	fire : function(){
		// this.set(); // こちらが正しい
		this.hoge();   // 実装ミス 
	},

	hoge : function(){}
};


fire()がset()に設定したメソッドを呼ぶ予定でしたが、
誤って、hoge() を呼んでいます。
このテストを以下の様に書いたとします。

test("set and fire",function(){
	expect(2); // これがないと、テストは赤にならない。
	ninja.set = function(){
		ok(true, "set was succeed");
	};
	ninja.fire(); // ここの実装ミスに気がつけない。

	ok(true, "another test");
});

ok(true, "another test") のために、set()に設定したok()が実行されなかったとしても、
テストはRedにはなりません。
これでは、fire()の実装ミスを見落とす可能性があります。
2つのアサーションを定義したつもりなので、expect(2)を設定すれば、ミスをあぶり出せます。


また、この「期待されるアサーションの数」は、test()とasyncTest()の第二引数として渡すことが出来ます。
つまり上の例は

test("set and fire",2 ,function(){
	// expect(2); // 代わりに引数で指定。
	ninja.set = function(){
		ok(true, "set was succeed");
	};
	ninja.fire();

	ok(true, "another test");
});

と書くことも出来ます。


expectedの値に対しては、アサーションが多くても少なくてもRedになるので、非同期関係やイベント関係、複雑なメソッド等のテストでは、「テストのテスト」のつもりで意識的に指定しておく習慣をつけると良いかもしれません。

module の使い方

module()はテストをグルーピングすることができます。
引数にメッセージを記述すると、そのメッセージを表示に使用します。

module("syugyou");

test("walk on the fire",function(){
	ok("win", true);
});

module("travel");

test("go to OISESAMA",function(){
	ok("arrived", true);
});



しかし、moduleは単にテストを分割するだけでなく、
そのグループのtestに対してsetUp(), tearDown() を定義することができます。
setUp(),tearDown()はそのモジュール内全てのテストの前後で実行されます。

module("fight",{
	setup: function() { // 初期化処理
		ninja.syuriken = 100;
		ninja.makibishi = 100;
	},
	teardown: function() { // 終了処理
		ninja.syuriken = 0;
		ninja.makibishi = 0;
	}
});


test("fight with Hanzou",function(){
	same(ninja.syuriken, 100, "syuriken is full"); // ちゃんと有る
	same(ninja.makibishi, 100, "syuriken is full"); // こっちも有る
	ninja.syuriken-=50;  // 使う
	ninja.makibishi-=30; // 使う
	ok(ninja.syuriken==50 && ninja.makibishi==70, "use a lot of wepon"); 
	ok(true, "win");
});


test("fight with Sasuke",function(){
	same(ninja.syuriken, 100, "syuriken is full"); // 補充されている
	same(ninja.makibishi, 100, "syuriken is full"); // 補充されている
	ninja.syuriken-=20;
	ninja.makibishi-=90;
	ok(ninja.syuriken==80 && ninja.makibishi==10, "use a lot of wepon"); 
	ok(true, "win");
});


test("fight with Saizou",function(){ // 以下同様
	same(ninja.syuriken, 100, "syuriken is full");
	same(ninja.makibishi, 100, "syuriken is full");
	ninja.syuriken-=100;
	ninja.makibishi-=100;
	ok(ninja.syuriken==0 && ninja.makibishi==0, "use a lot of wepon"); 
	ok(true, "win");
});

初期化と終了処理をキチンと書くことで、次のテストに与える影響をきちんとコントロールできます。

その他

QUnit-TAP : JavaScript のテスティングフレームワークQUnitからTAP出力する - t-wadaの日記こちらで知ったのですが、
まだドキュメント化されていないAPIが追加されています。
とくにこれに関する言及が開発側から一切ないようなので、今回のサンプルでは使っていません。
しかし、個人的にはequalとnotEqualなんか普通に使ってます。

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 が等価でないことを !== で検証します

まあ、これらだったらAPIが廃止になるってことも考えにくいので、普通に使っていいんじゃないでしょうか?
むしろequalsとかそのうち非推奨になったりしそう。

あとJUnitでいうassertThat()的なものがあってもいいかなぁとは思います。

まとめ

これだけわかると、基本的QUnitのテストを作成する上ではあまり問題ないかなと思います。
実際にはもっと多くの拡張が可能であったりと機能は豊富なんですが、それについては公式サイトをご覧下さい。


QUnit公式サイト


また、QUnitはこうしている間にもコミットが重ねられているくらい開発が進んでいます。
これからもっと色々な機能がつくかもしれませんが、現時点では必要最小限のものが揃った扱いやすいJSのテストフレームワークなんじゃないかと思います。


個人的には、QUnitで書いたテストをCI(Hudosonとか)で回せたらなぁと思っているのですが、未だに決定打が見つかっていません。
何か情報有りましたら教えて頂けるとうれしいです。