ラベル JsTestDriver の投稿を表示しています。 すべての投稿を表示
ラベル JsTestDriver の投稿を表示しています。 すべての投稿を表示

2013年5月13日月曜日

jgenhtmlでJsTestDriverのCoverageプラグインの出力をHTMLレポートに

JsTestDriverのCoverageプラグインは、LCOVフォーマットのファイルを出力する。「JsTestDriverでUnit Test + Code Coverage」では、Ubuntu上で実行していたからgenhtmlで簡単にHTMLレポートにできたけれど、Windows上ではgenhtmlを使えない。

Windows上でHTMLレポートに変換する方法について調べてみると、javascript - Viewing LCOV file in Windows - Stack Overflowにそのものズバリの質問が。回答を見てみると、Cygwinにgenhtmlをインストールする方法と、Java実装のjgenhtmlを使う方法があるらしい。

というわけで、今度はjgenhtmlを使ってみる。About JGenHtmlに書いてあるとおり、こちらを使うとパスの区切り文字("\"か"/"か) を気にしなくて良いし、JsTestDriverが動く (Javaがインストールされている) なら、jgenhtmlも動かせる。なお、動かしてみたjgenhtmlのバージョンは、1.5.0。

インストールも使い方も簡単。インストールは、jgenhtml-1.5.jar をダウンロードして、任意のフォルダに置けばよい。ここでは、「JsTestDriverでUnit Test + Code Coverage」のディレクトリ構成配下、test-lib/genhtmlに追加配置する。その時の使い方は、コマンドプロンプトで次のコマンドを実行する。
java -Dfile.encoding=UTF-8 -jar test-lib\jgenhtml\jgenhtml-1.5.jar -q -o test-output\coverage test-out\jsTestDriver.conf-coverage.dat
-Dfile.encodingの値は、JavaScriptファイルのエンコーディングに一致させること。そうしないと、HTMLレポートの日本語が文字化けする。jgenhtmlは、JVMのデフォルト・エンコーディングでJavaScriptファイルを読み込み、エンコードに関するオプションを持たないので、JVM側で指定している。エンコードにMS932以外を指定すると、今度はコマンドプロンプト上で文字化けするので、jgenhtmlのオプション-q(uiet)で、表示を止めている。

これで-o(utput)に指定したディレクトリにHTMLレポートが生成されるので、ブラウザで確認すれば良い。ただし、HTML5とCSS3を活用しているので、IEはサポート外とのこと。
The reports produced by jgenhtml use cutting edge HTML5 and CSS3 features.

If you are not using an extremely modern browser stuff probably won't work. If you use Internet Explorer my guess is you have no chance in anything before IE10.
BrowserSupport - jgenhtml - lcov genhtml tool ported to Java - Google Project Hosting
手元のIE9でざっと確認した範囲だと、IE8モードだとソースコードのハイライトを確認できるページがレイアウト崩れしている。

References

2013年5月6日月曜日

Sinon.JSのテストダブルを使ったユニットテスト

はじめに

JavaScriptでの、テストダブルを使ったユニットテストの書き方について書く。テストランナーにはJsTestDriver, モックライブラリにはSinon.JSを使う。

ベースとなるコードには、Sinon.JS > Getting startedから、Spies, Stubs, Testing Ajax, Fake XMLHttpRequest, Fake Serverの5つを使う。これらのコードはそのままでは実行できない (テストランナーにJasmineやMochaを使う場合のテストメソッドが切り出されている) ので、JsTestDriverで実行できるように書き換えて、サンプルコードとする。

書き換えの際、Sinon.JS > Documentationや『テスト駆動JavaScript』を参考に、次のTIPSを導入する。
  • JsTestDriverとSinon.JSのアサーションの統合
  • サンドボックスの導入
環境は次の通り。「JsTestDriverでUnit Test + Code Coverage」とほぼ同じ。
  • OS: Ubuntu 12.10 (Xubuntu)
  • エディタ: gedit
  • テストランナー: JsTestDriver 1.3.5
  • モックライブラリ: Sinon.JS 1.6.0
  • テストブラウザ: Firefox 20.0

Spies

最初にスパイの書き方。テストコードの書き換えは素直なので、ここでアサーションの統合についても書く。

テスト対象関数は次の通り。この関数は、引数に渡された関数を一度だけ実行し、結果をキャッシュする。
function once(fn) {
  var returnValue, called = false;
  return function () {
      if (!called) {
          called = true;
          returnValue = fn.apply(this, arguments);
      }
      return returnValue;
  };
}
これに対するJsTestDriverのテストコードは次の通り。テストケースの書き換え自体には、特に注意するところはないと思う。

注意すべきは、アサーションの書き方。3つのテストケースで、書き方を変えている。1つ目と2つ目のテストメソッドの書き方は、統合不要。3つ目のテストメソッドの書き方には、統合が必要。それぞれの特徴はコメントを参照。
// JsTestDriverとSinon.JSのアサーションの統合
// 普通は全テストケースで共有するために、グローバルヘルパーで実行する
sinon.assert.expose(this);
 
TestCase('SpyExample', {
  'test calls the original function':function() {
    var callback = sinon.spy();
    var proxy = once(callback);
 
    proxy();

    // JsTestDriverのアサーションを使う場合。メッセージが不親切
    assertTrue(callback.called);
  },
 
  'test calls the original function only once':function() {
    var callback = sinon.spy();
    var proxy = once(callback);
 
    proxy();
    proxy();
 
    // Sinon.JSのアサーションを完全修飾して使う場合。メッセージがフレンドリィ
    sinon.assert.calledOnce(callback);
  },
 
  'test calls original function with right this and args':function() {
    var callback = sinon.spy();
    var proxy = once(callback);
    var obj = {};
 
    proxy.call(obj, 1, 2, 3);
    // Sinon.JSのアサーションを統合して使う場合。
    assertCalledOn(callback, obj);
    assertCalledWith(callback, 1, 2, 3);
  }
});
なお、統合は、sinon.assert.exposeで行っている。メソッド名などオプション引数でカスタマイズできるので、詳しくはリンク先を参照のこと(JsTestDriver以外のテストランナーと統合する場合にも要参照。アサーションを持つオブジェクトによって引数に渡すべき値を変えたり、統合先のテストランナーの失敗の扱い方によってsinon.assert.failのオーバーライドしたりする必要がある)。

Stubs

次に単純なスタブの書き方。テスト対象関数はSpiesと同じ。

テストコードは次の通り。これも特に注意するところはないと思う。
TestCase('StubExample', {
  'test returns the return value from the original function':function() {
    var callback = sinon.stub().returns(42);
    var proxy = once(callback);
 
    assertEquals(42, proxy());
  }
});

Testing Ajax

Ajaxスタブの書き方。ここではサンドボックスを導入する。

テスト対象関数は次の通り。Ajaxを簡単に取り扱うために、jQueryを使っている。
function getTodos(listId, callback) {
  $.ajax({
    url: "/todo/" + listId + "/items",
    success: function (data) {
      // Node-style CPS: callback(err, data)
      callback(null, data);
    }
  });
}
これに対するテストコードは次の通り。ポイントは2行目。スタブ化したグローバルオブジェクト$を復元しないと、他のコードに影響するかもしれない。そこで、sinon.testでテストメソッドをラップしてサンドボックス化している。
TestCase('AjaxExample', {
  'test makes a GET request for todo items':sinon.test(function(stub) {
    this.stub($, 'ajax');
    getTodos(42, sinon.spy());
 
    assertTrue($.ajax.calledWithMatch({url: '/todo/42/items'}));
  })
});
スタブへのアクセスは、this.stubで行う。『テスト駆動JavaScript』ではthisがないけれど、それだと動作しなかった。バージョンアップで変更になったのだと思う。

サンドボックス化すべきテストメソッドが複数ある場合は、代わりにsinon.testCaseでテストクラスをラップできる。次の次のFake Serverで使ってみる。

Fake XMLHttpRequest

続いて、XMLHttpRequestのスタブ。あえてサンドボックスを使わないで書くと、復元が面倒になるという例に使う。テスト対象関数は、Testing Ajaxと同じ。

テストコードは次の通り。setUp, tearDown内でテストダブルを自前で管理しなければならない。
TestCase('FakeXMLHttpRequestExample', {
  setUp: function() {
    this.xhr = sinon.useFakeXMLHttpRequest();
    var requests = this.requests = [];
 
    this.xhr.onCreate = function(req) {
      requests.push(req);
    };
  },
 
  tearDown: function() {
    // Like before we must clean up when tampering with globals
    this.xhr.restore();
  },
 
  'test makes a GET request for todo items': function() {
    getTodos(42, sinon.spy());
 
    assertEquals(1, this.requests.length);
    assertEquals('/todo/42/items', this.requests[0].url);
  }
});

Fake server

最後に、サーバのスタブ。テストケースのサンドボックス化を導入する。なお、これもテスト対象関数は、Testing Ajaxと同じ。

テストコードは次の通り。テストケースをサンドボックス化する場合はこうなるはず(ドキュメントに沿えばこうなると解釈したコードで、テストのパスは確認したがテストダブルの復元までは未確認)。
TestCase('FakeServerExample', sinon.testCase({
  'test calls callback with deserialized data': function(server) {
    this.server = sinon.fakeServer.create();
    var callback = sinon.spy();
    getTodos(42, callback);
 
    // This is part of the FakeXMLHttpRequest API
    this.server.requests[0].respond(
      200,
      {'Content-Type': 'application/json'},
      JSON.stringify([{id: 1, text: 'Provide example', done: true}])
    );
 
    assert(callback.calledOnce);
  }
}));

References

JsTestDriverでテストケースを書く

JsTestDriverで実行するためのテストケースの書き方について記載する。使うバージョンは、1.3.5。テストターゲットが同期処理の場合と非同期処理の場合のそれぞれについて、JsTestDriverのプロジェクトWikiをベースに記載する。なお、テストの実行方法については、「JsTestDriverでUnit Test + Code Coverage」に記載した。

テストケースの書き方には、次の2通りがある。Getting Started with JsTestDriverでは、プロトタイプを使っているが、『JavaScript実践入門』に習って、インライン宣言を使う。インライン宣言だと、テスト名に任意の文字列が使える。また、記述量も少なくなる。
  1. プロトタイプを使う
  2. インライン宣言を使う
Getting Started with JsTestDriverのサンプルコードを元に、インライン宣言を使ってテストケースのスケルトンを書くと、次のようになる。
TestCase('GreeterTest', {
  setUp:function() {
    // 必要に応じてセットアップ処理を実装する。
  },

  'test greet returns Hello World!':function() {
    // Set up
    var sut = new myapp.Greeter();

    // Exercise
    var actual = sut.greet('World');

    // Verify
    assertEquals('Hello World!', actual);
  },

  tearDown:function() {
    // 必要に応じてティアダウン処理を実装する。
  }
});

テストターゲットが非同期の場合は、TestCaseクラスではなくて、AsyncTestCaseクラスを使う。AsyncTestCase - js-test-driverのサンプルコードを元に、インライン宣言を使って書き直すと次のようになるはず(JsHintはおおよそ通したが未実行なので、修正が必要かもしれない)。また、高度な内容(コールバック関数の実行タイミングの制御など)はAsyncTestCase - js-test-driverを参照のこと。
AsyncTestCase('XhrTest', {
  'test XHR using callbacks':function(queue) {
    // Set up
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/some/path');

    var responseStatus;
    var responseBody;
    
    queue.call('Step 1: send a request to the server and save the response status and body', function(callbacks) {
      var onStatusReceived = callbacks.add(function(status) {
        responseStatus = status;
      });
      
      var onBodyReceived = callbacks.add(function(body) {
        responseBody = body;
      });

      xhr.onreadystatechange = function() {
        if (xhr.readyState === 2) { // headers and status received
          onStatusReceived(xhr.status);
        } else if (xhr.readyState === 4) { // full body received
          onBodyReceived(xhr.responseText);
        }
      };

      // Exercise
      xhr.send(null);
    });

    // Verify
    queue.call('Step 2: assert the response status and body matches what we expect', function() {
      assertEquals(200, responseStatus);
      assertEquals('hello', responseBody);
    });
  }
});

References

2013年5月4日土曜日

JsTestDriverでUnit Test + Code Coverage

はじめに

JsTestDriverで、JavaScriptのユニットテストを実行し、htmlレポートでカバレッジを確認する方法。環境は次の通り。
  • OS: Ubuntu 12.10 (Xubuntu)
  • エディタ: gedit
  • テストランナー: JsTestDriver 1.3.5
  • テストブラウザ: Firefox 20.0
  • カバレッジ・レポーター: LCOV 1.9
アウトラインは次の通り。なお、コードの書き方については深入りしない。JsTestDriverのProject Wikiを参照のこと。
  1. 初期設定
  2. 実行準備
  3. 実行
    • パスの場合
    • 失敗してデバッグの場合
  4. カバレッジ確認

初期設定

ディレクトリ構成は下記の通りとする。jarの配置がJsTestDriverおよびカバレッジ・プラグインのインストールに相当する。プロダクトコードgreeter.jsとテストコードgreetertest.jsの内容については、Getting Started with JsTestDriverを参照のこと。
.
├─ jsTestDriver.conf
├─ src
│   └─ greeter.js
├─ test-lib
│   └─ jstestdriver
│       ├─ JsTestDriver-1.3.5.jar
│       └─ plugins
│           └─ coverage-1.3.5.jar
├─ test-output
│   └─ coverage
└─ test-src
     └─ greetertest.js

設定ファイルjsTestDriver.confの内容は次のようになる。jsの読み込み順序をコントールしたい場合など、詳細については、ConfigurationFileを参照のこと。
server: http://localhost:4224

load:
  - src/*.js

test:
  - test-src/*.js

plugin:
 - name: "coverage"
   jar: "test-lib/jstestdriver/plugins/coverage-1.3.5.jar"
   module: "com.google.jstestdriver.coverage.CoverageModule"

実行準備

ユニットテストを実行する前に、テストサーバを起動しブラウザをキャプチャする必要がある。まずテストサーバを起動するために、シェルで次のコマンド実行する。
# オプション--portをjsTestDriver.confで指定したポート番号に一致させる
$ java -jar test-lib/jstestdriver/JsTestDriver-1.3.5.jar --port 4224
続いて、ブラウザからhttp://localhost:4224/captureにアクセスする。

コマンドライン・フラグで、シェルからテストサーバを起動すると同時に、ブラウザをキャプチャすることもできる。詳細については、CommandLineFlagsを参照のこと。

実行

シェルで次のコマンドを実行する。
$ java -jar test-lib/jstestdriver/JsTestDriver-1.3.5.jar --tests all --testOutput test-output

パスの場合

テストをパスするなら、次のような結果が返ってくる。カバレッジはファイルにしか出力されない。簡易にコンソールで確認したいなら、オプション--testOutputを外しておく。
setting runnermode QUIET
Firefox: Reset
Firefox: Reset
.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (0.00 ms)
  Firefox 20.0 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (0.00 ms)
加えて、test-outputにテスト結果(JUnit XML形式)とカバレッジ(LCOV互換)が出力される。
  • TEST-Firefox_200_Linux.GreeterTest.xml
  • jsTestDriver.conf-coverage.dat

失敗してデバッグの場合

失敗すると、次のような結果が返ってくる。カバレッジ用と思われるLCOV.jsの出力がうるさいけれど、カバレッジ有無を簡単に切り替えられるかどうか未確認。
setting runnermode QUIET
Firefox: Reset
Firefox: Reset
F
Total 1 tests (Passed: 0; Fails: 1; Errors: 0) (1.00 ms)
  Firefox 20.0 Linux: Run 1 tests (Passed: 0; Fails: 1; Errors 0) (1.00 ms)
    GreeterTest.testGreet failed (1.00 ms): AssertError: expected "Hello World!" but was "Hell World!"
      GreeterTest.prototype.testGreet@http://localhost:4224/test/src-test/greeter_test.js:7
      runTest@http://localhost:4224/test/com/google/jstestdriver/coverage/javascript/LCOV.js:203
      TestResultIterator.prototype.runNext@http://localhost:4224/test/com/google/jstestdriver/coverage/javascript/LCOV.js:292
      InstrumentedTestCaseRunner.prototype.run@http://localhost:4224/test/com/google/jstestdriver/coverage/javascript/LCOV.js:250
      InstrumentedTestCaseRunnerPlugin.prototype.runTestConfiguration@http://localhost:4224/test/com/google/jstestdriver/coverage/javascript/LCOV.js:221

失敗したメソッドをデバッグするため、そのメソッドのみを実行するために、シェルで次のコマンドを実行する。
$ java -jar test-lib/jstestdriver/JsTestDriver-1.3.5.jar --tests GreeterTest.testGreet
デバッグするのにconsole.log()で十分なら、コマンドラインフラグに --captureConsoleを付けてテストを実行すればよい。

ブレークポイントが必要なときは、ブラウザのデバッガを利用する。そのためには、キャプチャしたブラウザのデバッガ (Firefoxなら[ツール] > [Web開発] > [デバッガ]、あるいはFireBug) を開き、ブレークポイントを設定すればよい。再度、上記コマンドを再実行すると、ブレークポイントで停止するので、デバッグできる。

console.log()にせよブラウザのデバッガにせよ、ここでもカバレッジ用のコードが挿入されて煩わしいが、カバレッジ有無を簡単に切り替えられるかどうか未確認。

カバレッジ確認

カバレッジデータjsTestDriver.conf-coverage.datの/./を/に置換しておく。本来的には不要な作業だが、置換しておかないとカバレッジのhtmlレポートがリンク切れを起こす。恐らくIssue 367と同じ問題が発生していると思われる。

htmlレポートを生成するには、シェルで下記コマンドを実行する。test-output/coverage以下に多数のファイルが生成されるが、index.htmlがエントリーポイント。genhtmlの詳細については、Linux Test Project - Coverage » lcovを参照のこと。
$ genhtml -o test-output/coverage -f test-output/jsTestDriver.conf-coverage.dat

雑感

カバレッジを測ろうとすると、出力が汚くなる。特にデバッグコードに測定用コードが挿入されてしまうのが煩わしい。簡単に切り替えられると良いのだけれど、カバレッジ有無がコマンドライン・フラグではなくて設定ファイルのようなので面倒。設定ファイルの切り替えにしようとすると、ほぼ複製になってしまう。

Issue 367のgenhtmlで生成するHTMLレポートのリンクが切れる問題は、置換してから渡すようなワンライナーで当座はしのげそう。jsTestDriverというよりLCOVの問題だけれど、変更履歴を見ると1.10では解決されていそう。
- Fixed directory prefix calculation
Linux Test Project - Coverage » lcov