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

0 件のコメント:

コメントを投稿