2013年5月18日土曜日

JavaScriptで依存性を排除するためにデフォルトパラメータを導入する

JavaScriptのクラスHogeをテストしたいのだけれど、次のような状況でそもそもインスタンス化が大変な場合にどうするか? という話。
  • Hogeはコンストラクタ内で別のクラスFugaをインスタンス化してプロパティに設定している。
  • Fugaはインスタンス化の際にサードパーティ・ライブラリのAPIを大量に呼び出している。
  • サードパーティ・ライブラリは、開発環境では使えない(例えば、REST APIを提供するサーバがまだ立っていない)。
この場合をコード例で表してみる。サードパーティ・ライブラリが使えないため、Fugaのコンストラクタ呼び出しでエラーが発生し、Hogeをインスタンス化できないとする。
Hoge = function () {
  this.fuga = new Fuga();
  // ...
}
// ...
Fuga = function () {
  this.thirdPartyLibrary = new ThirdPartyLibrary();
  // ...
}
// ...

FugaがThirdPartyLibraryをラップしているから、テスト時にはテストダブルに入れ替えたい。でも、そもそもHogeもFugaもインスタンス化できない。

こんな状況に対応するためのリファクタリングとして、『レガシーコード改善ガイド』は25.14「コンストラクタのパラメータ化」か、デフォルト引数の追加を紹介している。オーバーロードのないJavaScriptでは、コンストラクタをパラメータ化できないので、デフォルト・パラメータを指定する。

デフォルト・パラメータがtrueと見なせる(false、null、0、""、undefinedのいずれでもない)なら、こんな風に短く書ける。||は真偽値ではなくてfugaを返す性質を利用している。
Hoge = function (fuga) {
  this.fuga = fuga || new Fuga();
  // ...
}
// ...

もっとロバストな書き方は次の通り。こちらは、デフォルト・パラメータがfalseと見なされる場合も使える。実用的なのは、デフォルト値が0やfalseの時。
Hoge = function (fuga) {
  if (typeof fuga === 'undefined') {
    fuga = new Fuga();
  }
  this.fuga = fuga;
  // ...
}
// ...

References



JavaScriptにおける検出用変数

JavaScriptで『レガシーコード改善ガイド』の第22章「モンスターメソッドを変更する必要がありますが、テストを書くことができません」で紹介されている「検出用変数の導入」を行ってみる。

『レガシーコード改善ガイド』のサンプルコードはJavaだからインスタンス変数として導入しているけれど、ここでは関数プロパティとして導入する。JavaScriptでは、関数もオブジェクトだからプロパティを持つことができる。

関数プロパティとして導入した方が、使用する場所に近くなるから読みやすくなるし、何かの拍子に誤ってアクセスする可能性が小さくなる。モンスターメソッドはただでさえ長い上に、そいつを持っているオブジェクトもゴッド・オブジェクトだったりするから、どれだけ慎重になってもなり過ぎるということはないはず。

というわけで、godObject.monsterMethodに検出変数isProcessedを導入してみる。これで複雑な条件をかいくぐって目的のブロックが実行されているかどうかを検出できるようになる(この実行が委譲されているなら、モックを使って検出できるけれど、モンスターメソッドが書かれるような状況では期待薄だと思う)。
var godObject = {
    monsterMethod : function (num) {
        // 検出用変数
        this.monsterMethod.isProcessed = false;
    
        if (true) {
            // ... 
            if (false)  {
                // ...
            } else if (true) {
                if (num === 2) {
                    this.monsterMethod.isProcessed = true;
                    // このブロックが実行されているかどうかを検出したい
                }
            } else {
                // ...
            }
        }
    }
}

検出用変数には、godObject.monsterMethod.isProcessedでアクセスできる。実際には、テストコード中でassertTrue(godObject.monsterMethod.isProcessed)のような形でアクセスすることになるだろうけれど、ここでは簡単に確認するためにconsole.logを使う。
godObject.monsterMethod(1);
console.log(godObject.monsterMethod.isProcessed); // > false
godObject.monsterMethod(2);
console.log(godObject.monsterMethod.isProcessed); // > true
godObject.monsterMethod(3);
console.log(godObject.monsterMethod.isProcessed); // > false

この方法は、『JavaScriptパターン』の4.3「関数プロパティによるメモ化パターン」を参考にしている。関数にその関数のキャッシュ変数を持たせられるなら、検出用変数を持たせられるだろう、と。

References


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

WindowsのコマンドプロンプトからJSHint

WindowsのコマンドプロンプトからJShintを実行する方法について書く。いちいちjshint.comのテキストエリアに貼り付けていては、リズムが悪いし、多数のファイルを一度にLintできない。

方法は次の2つが考えられたけれど、『メンテナブルJavaScript』に倣って、後者を選んだ。ざっと検索した感じ、Rhinoをインストールする方が、Nodeとnpmをインストールするより手軽そうだったのも理由の1つ(特にnpmのインストールには、GitとPythonが必要)。

Rhino bundleを使う場合のデメリットは、大きく2つ。前者は規定のオプションを渡すバッチファイルを書けば、代用できると思う。後者は替えが効かないので、将来対応されると嬉しい。
  • 設定ファイルを読み込めない。オプションはコマンドライン引数として渡さないといけない(ソースを直接いじるという荒技もできそう)。
  • コマンドラインフラグを使えない。コマンドラインフラグはNode版しか使えない。

まず、次の環境を構築する。Windows 7へのJava 7のインストールは済んでいるものとする。Rhinoは、Download RhinoからBinariesを、ダウンロードして任意のフォルダに展開すれば良い。JSHintは、Install — JSHintから、Rhino bundleをダウンロードして、任意のフォルダに保存すれば良い。以下、RhinoはRHINO_HOMEに、JSHintはJSHINT_HOMEにインストールしたとする。
  • OS: Windows 7
  • Rhino実行環境: Java 7
  • JSHint実行環境: Rhino 1.7R4
  • JShint (Rhino bundle): jshint-rhino-2.0.1.js

実行するには、次の形式でコマンドを実行すれば良い。-Dfile.encodingの値は、Lint対象のJavaScriptファイルのエンコーディングに一致させること。指定できるオプションについては、JSHint optionsを参照のこと。
java -Dfile.encoding=UTF-8 -jar RHINO_HOME\js.jar JSHINT_HOME\jshint-rhino-2.0.1.js [opt1=val1,opt2=val2,...] [global1=true,global2,global3,...] [list of files]
なお、オプションの書式は、jshint-rhino-2.0.1.jsの11000行目から始まる無名関数のコメントから抜粋。どうやら"="の有無でオプションかどうか判定している。以下はオプションの例。
camelcase=true,curly=true,eqeqeq=true,immed=true,indent=4,latedef=true,newcap=true,nonew=true,quotmark=single,strict=true,unused=true,undef=true,trailing=true,maxlen=80

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

2013年5月2日木曜日

JavaScriptのUnit Test Tool

まとめた先から情報が古くなりそうで躊躇していたけれど、JavaScriptのUnit Testツールについて、最近見かけた情報をまとめてみる。


『JavaScript Unit Test Why? What? How?』はUnit Testツールを次の4レイヤで分離している。特定レイヤのみの機能を提供するツールもあれば、複数レイヤの機能を提供するツールもある。図でテスティングフレームワークがさらに分かれているのは、Mochaが好みのAssertionライブラリ (Chaiなど)を使える設計になっているため。
  • モックライブラリ
  • テスティングフレームワーク
  • リモートテストランナー
  • 実行環境

これらのツールは組み合わせて使うことができる。フレームワークで実践! JavaScriptテスト入門では、次の4パターンを紹介している。また、『JavaScriptの開発効率を高める7つのライブラリ』では、JasmineとSino.JSを組み合わせている。

上記連載の著者のスライド『JavaScriptテストフレームワークを諸々眺めてみる』では、上記の他に次のテストフレームワークを紹介している。

また、モックライブラリSinon.JSについては、Sinon.JSが詳しい。『JavaScriptの開発効率を高める7つのライブラリ』では、モック機能を持つJasmineと組み合わせているし、Sinon.JSが定番という理解でよさそう。

この他のツールとして、上記リンク先の複数がBuster.JSを挙げている。Buster.jsはJsTestDriverと同じレイヤをカバーしていて、Sinon.JSをバンドルしているので、カバー範囲が広い。Vowというものもあるらしい。それから、Yahoo!のYUI TestがIBM developerWorksで紹介されている。

書籍に目をやると、定番とおぼしき2011年発売の『テスト駆動JavaScript』で主に取り扱っているのは、JsTestDriver。それから、最近日本語訳が発売された『メンテナブルJavaScript』では「19章 自動テスト」で次の4つのツールを紹介している。どちらも未読なので、どれくらい詳しく紹介されているかは未確認。
  • YUI.Test.Selenium.Driver
  • Yeti
  • PhantomJS
  • JsTestDriver

上記をざっと見ると、jsTestDriverの支持率が高そう。それから、比較の際に着目されているのは、次の4点。個人的には、加えて出力形式も気になるところ。
  • スタイル: TDD? BDD?
  • 実行環境: 実ブラウザ? ヘッドレスブラウザ?(=Phantomjs) シミュレータ?
  • 非同期対応
  • CI対応

以下、余談。別の観点では、そもそもテスタビリティの低いコードは、悪いコードだというわけで、Lintや静的解析ツールも有用そう。Lintだと、JSLint, JSHint, Closure Linterが有名そう。静的解析ツールはあまり見当たらない。jsmeter, complexityReport.jsあたりか?

2013/05/05追記:
静的解析ツールにplatoというツールも見つかった。

2013/05/09追記:
カバレッジ計測に限れば、JSCoverというJSCoverageの後継のツールもある。