連載
» 2013年01月21日 18時00分 公開

WebブラウザでJavaScriptをテストする「js-test-driver」とQUnit、Jasmineを連携してテストするにはフレームワークで実践! JavaScriptテスト入門(4)(3/4 ページ)

[金岡徹,TIS コーポレート本部 戦略技術センター]

js-test-driverと「QUnit」を連携させるには

 ここからは、js-test-driverと前回紹介した「QUnit」の連携を紹介します。js-test-driverとQUnitを連携させるには「QUnitAdapter」を利用します。

QUnitAdapterのセットアップ

 QUnitAdapterで必要となるファイルは「QUnitAdapter.js」「equiv.js」です。これらを「tests/qunit」というフォルダを作成し、配置します。

 ファイルを配置したら、QUnitAdapter.jsとequiv.jsを読み込ませるためにjsTestDriver.confに以下のように指定します。

@@ -1,6 +1,8 @@
 server: http://localhost:9876
 
 load:
+  - tests/qunit/equiv.js
+  - tests/qunit/QUnitAdapter.js
   - src/*.js
 
 test:
jsTestDriver.conf

 これでセットアップは完了です。次にプロダクションコードとテストコードを見ていきます。

テストコードをQUnitに変更する

 基本的にプロダクションコードは変更する必要はないので、テストコードを修正します。js-test-driver用に書いたコードをQUnitがサポートしている形式で書き直します。

module("GreetTest");
test("GreeterTest", 1, function(){
    var greeter = new myapp.Greeter();
    equal("Hello World!", greeter.greet("World"), "retrun to expected data");
})
src-test/greetertest.js

 QUnitAdapterではQUnitでサポートしている機能すべてが利用できるわけではありません。Assertも制限されており、以下のAssertのみ利用可能です。

  • expect
  • ok
  • equals(equal)
  • same(deepEqual)

 equalやdeepEqualのテストも可能ですが、Assertがそれぞれ「equals」「same」という関数名になっているのに注意してください。

equals(greeter.greet("World"), "Hello World!", "retrun to expected data");
正しい例
equal(greeter.greet("World"), "Hello World!", "retrun to expected data");
誤っている例

 また、前回の記事で説明した非同期処理のテストを実行するための「start()」「stop()」は利用できないため、他の手段を使って実現する必要があります。

QUnitAdapter.jsのカスタマイズ

 このままだと不便なので、「equal」「deepEqual」を利用できるようにQUnitAdapter.jsをカスタマイズします。QUnitAdapter.jsではQUnitで利用するAssertをjs-test-driverで利用可能なAssertにマッピングしているだけなので、そこを少し変更してみます。

 QUnitAdapter.jsを修正する前にプロダクションコードとテストコードを以下のように書きます。プロダクションコードには「deepEqual」を検証するための「hello」関数を作成し、テストコードは「equal」「deepEqual」を検証するコードを追加します。

@@ -3,5 +3,10 @@ 
myapp.Greeter.prototype.hello = function() {
    return { foo: "bar" };
};
src/greeter.js
@@ -1,5 +1,6 @@
module("GreetTest");
test("greetTest", 1, function(){
    var greeter = new myapp.Greeter();
    equal(greeter.greet("World"), "Hello World!", "retrun to expected data");
})
test("helloTest", 1, function(){
    var greeter = new myapp.Greeter();
    deepEqual(greeter.hello(), { foo: "bar" }, "Two objects can be the same in value");
})
src-test/greetertest.js

 ここからQUnitAdapter.jsを以下のように修正します。

@@ -75,6 +75,40 @@ you need to set it up and tear it down in each test.
         return false;
     };
    
+    window.equal = function(a, b, msg) {
+        assertEquals(msg ? msg : '', b, a);
+    };
+
+    window.deepEqual = function(a, b, msg) {
+       if (window.equiv(b, a) == true ) {
+           assert(msg ? msg : '', window.equiv(b, a));
+       } else {
+           fail(msg + ' expected ' + prettyPrintEntity_(b) + ' but was ' + prettyPrintEntity_(a) + '');
+       }
+    };
+    function prettyPrintEntity_(entity) {
+       if (isElement_(entity)) {
+           return formatElement_(entity);
+       }
+
+       var str;
+
+       if (typeof entity == 'function') {
+           try {
+               str = entity.toString().match(/(function [^\(]+\(\))/)[1];
+           } catch (e) {}
+
+           return str || '[function]';
+       }
+
+       try {
+           str = JSON.stringify(entity);
+       } catch (e) {}
+
+       return str || '[' + typeof entity + ']';
+    }
+
     window.QUnit = {
         equiv: window.equiv,
         ok: window.ok
tests/qunit/QUnitAdapter.js

 「equal」は「equals」をリネームしているだけです。「deepEqual」は、テストに失敗したときに以下のように「expect」「actual」の値がtrueもしくはfalseで出力されてしまいます。

AssertError: Two objects can be the same in value expected true but was false

 これでは失敗したときに修正するコストが高くなってしまうので、fail関数を呼び出して「expect」「actual」の値が正しく出力されるように修正しています。

 テストが成功した場合は以下のように表示されます。

 また、テストが失敗した場合は以下のように出力されます。

「jsUnitMockTimeout.js」と連携して非同期処理のテストを試す

 ちなみに、このままだと「start()」「stop()」が使えないため、現状では非同期処理のテストができません。QUnitAdapter.jsのソースコードにも書かれているように、「start()」「stop()」の代替えとして「jsUnitMockTimeout.js」ライブラリを導入することで、テストができます。

 ブログ「jsUnit Mock Timeouts and JS Test Driver - Blog - monket.net」で紹介されているダウンロードリンクから、jsUnitMockTimeout.jsを取得し、tests/qunit以下に配置します。

 jsTestDriver.confは以下の修正でOKです。

@@ -3,6 +3,7 @@ server: http://localhost:9876
 load:
   - tests/qunit/equiv.js
   - tests/qunit/QUnitAdapter.js
+  - tests/qunit/jsUnitMockTimeout.js
   - src/*.js
jsTestDriver.conf

 プロダクションコードとテストコードのサンプルを以下に示しています。setTimeoutを利用してる「asyncFunc」関数を検証しています。

myapp.Greeter.prototype.asyncFunc = function(status) {
    status.message = "start"
    setTimeout(function(name){
        status.message = "done";
    }, 2000);
}
src/greeter.js
module("async");
test("asyncFuncTest", 2, function(){
    var greeter = new myapp.Greeter();
    Clock.reset();
    var status = {};
    greeter.asyncFunc(status);
    equal(status.message, "start", "return to expected data.");
    Clock.tick(2000);
    equal(status.message, "done", "return to expected data.");
})
src-test/greetertest.js

 「Clock.tick()」を利用することによって「setTimeout」をうまく処理できます。「src/greeter.js」のsetTimeout関数の第2引数を2000としていますが、3000とすると第1引数の関数が実行されていないためテストは失敗し、以下のようなメッセージが表示されます。


js-test-driver単体で非同期処理をテストするには

 js-test-driver単体で非同期処理をテストする場合は、TastCaseクラスの代わりにAsyncTestCaseクラスを利用して実現します。詳細は「AsyncTestCase - js-test-driver - Demonstrates usage of the asynchronous testing API. - Remote javascript console - Google Project Hosting」を参照してください。

 AsyncTestCaseは「queue」という引数付きの関数を利用でき、「queue.call(function(callbacks) {})」で処理をステップ実行できます。

+AsyncTest = AsyncTestCase("asyncTest");
+AsyncTest.prototype.testGreet = function(queue) {
+    var greeter = new myapp.Greeter();
+    var status = {};
+    var message = "start"
+
+    queue.call('Step1', function() {
+           greeter.asyncFunc(status);
+           assertEquals(message, status.message);
+    });
+    
+    queue.call('Step2', function(callbacks) {
+           var mc = callbacks.add(function(){ message = "done" });
+           window.setTimeout(mc, 1000);
+    });
+
+    queue.call('Step3', function() {
+           assertEquals(message, status.message);
+       });
+};
src-test/greetertest.js

 処理内でsetTimeoutといった時間のかかる処理が終わってから次のステップに移りたい場合は、callbacksにコールバック関数を追加することで、処理の終了を待ってから次のステップに移ることができ、値の検証ができるようになります。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。