はじめに
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のアサーションの統合
- サンドボックスの導入
- 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); } }));
0 件のコメント:
コメントを投稿