sinon.jsを使うと、XMLHttpRequestをテスト用のオブジェクト(実際に通信は行わない)に置き換えることができる。
これがすごい便利で良いのだけど、fakeServerを使った時にはまった。
たとえば、hogeClientを実行すると、なんやかんやあってsomeObjectのsomemethodが呼び出されることをテストしたいとする。
var server = sinon.fakeServer.create();
server.autoRespond = true;
server.respondWith( function( xhr, id ) {
xhr.respond( 200, null, "..." );
} );
spyOn( someObject, "somemethod" );
hogeClient.request( ... );
expect( someObject.somemethod ).toHaveBeenCalled();
といった感じのテストコードになるだろう(実際にはserverの定義とかはbeforeEachとかでやるだろうけど)。autoRespondがtrueになっていると、リクエストを送ると自動的にrespondWithの引数で渡した処理に従い、用意したレスポンスが返ってくる。
やってみればわかるけど、someObject.somemethod内に適当なconsole.logを仕込んでおいて実行すると、console.logの出力的にはsomemethodが呼ばれているのに、toHaveBeenCalledのテストは失敗する。
なんてかなーと思ってsinon.jsのソースコードを見てみる(L3624辺り)と、autoRespondの場合、setTimeoutでレスポンスが返るタイミングが微妙にずれていた。
となると、hogeClient.request()を実行し、その次のexpectを評価した後に、レスポンスが返ってきてコールバックやら何やらが実行されることになるので、当然expect時点はテスト対象のメソッドは一度も呼び出されていないことになる。
なので、jasmineで書くのであれば、非同期処理のテストを書くのと同じように、
runs( function() {
var server = sinon.fakeServer.create();
server.autoRespond = true;
server.respondWith( function( xhr, id ) {
xhr.respond( 200, { "content-type": "application/json" }, JSON.stringify( { entries: { ... } ) );
} );
spyOn( someObject, "somemethod" );
hogeClient.request( ... );
} );
waitsFor( function() {
return someObject.somemethod.wasCalled;
}, "timeout", 3000 );
runs( function() {
expect( someObject.somemethod ).toHaveBeenCalled();
} );
となる。今回のような単純な例だと、waitsForの中でwasCalledを呼んでいる時点で確実にその後のテストは通るので、その後のexpectはあまり意味ないかもしれないが、expect書かないとテストにならんので。
同じような機能でfakeXMLHttpRequestがあるのだけど、これの時はhogeClient.request実行後に明示的にfakeXMLHttpRequestにレスポンスを返すように指示するので、非同期処理の実装を同期的なテストコードでテストできる。そこから推測して、autoRespondを指定すると同期的に返してくれると思ったのが間違い。
ちなみに、1.7.3だとsinon.jsにバグがあって、somemethodの呼び出し回数が1回である、というようなexpectをすると、テストは失敗する。これは、https://github.com/cjohansen/Sinon.JS/pull/320 にissueとして登録されてる現象で、onloadが2回呼ばれるため、結果的にsomemethodも2回呼ばれることになる。もうmasterにmergeされているみたいなんで、そのうち解消されるでしょう。