こんにちは。
最近、Backbone.jsというライブラリを使って、制作をしています。
Backboneいいですね〜。
各UIパーツの結合度が下がるので、
全体の見通しが良くなり、メンテもしやすくなります。
今作っているものはそこまで規模が大きいものではないのですが、
大規模js開発入門ということで。
それに加えて、先日JavaScript道場に行ってきてから、
jsの開発でもテストコードを書くようにしています。
師範に習ったとおり、
mocha + expectjs + _sinonjs_を用いてユニットテストを書いているのですが、
そのテストを書いている時に、sinonjsのspyで詰まったのでメモ。
そもそも、sinonjsとは何か。
そしてその中のspyという機能は何なのかをざっと。
sinonjsとは、テストダブルのライブラリのことです。
テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。 – テストタブル – wikipedia
フロントエンドJavaScriptにおける設計とテスト
すごくざっくり言うと、テスト用の便利なライブラリです。
そして、そんなsinonjsのspyという機能は、 その名の通りスパイをしてくれます。
何のスパイをするかというと、任意の関数に忍び込ませて、
などなどを調べることが出来るのが、spyです。
ざっくりした使い方を書くと、
it('sinon.spyのテスト', function() {
var hoge, spy;
hoge = {
foo: function() {
return true;
}
};
spy = sinon.spy(hoge, 'foo');
hoge.foo();
return expect(spy.calledOnce).to.be.ok();
});
という感じに書けます。
spy = sinon.spy( object, 'proterty' );
というふうに書くと、object.propertyを監視出来ます。 この感じで、Backboneのon系のコールバックも見えるんじゃないかと思ったら、詰まりました。
動かなかったコードを簡略化したものがこちらです。
少し長いのでCoffeeScriptで書きます。
describe 'Backbone * sinon.spy', ->
Model = Backbone.Model.extend
defaults:
name: 'hoge'
View = Backbone.View.extend
initialize: ->
_.bindAll @, 'render'
@model.on 'change:name', @render
render: ->
@$el.html @model.get('name')
before ->
@view = new View(model: new Model())
@spy = sinon.spy(@view, 'render')
after ->
@spy.restore()
it 'Modelモデルが変更された時View.renderが呼ばれる', ->
@view.model.set('name', 'leko') expect(@spy.calledOnce).to.be.ok()
実行してみると、通りません。。
Backboneを理解されてる方なら
「初心者乙」
で終わってしまうのかもしれませんが、Backbone初心者だから仕方ない。
先ほどの例のように、
spy = sinon.spy(view, 'render')
と指定したので、
view.renderが呼ばれたらspy.calledOnceはtrueになるはず。
console.logなどを挟んで関数が呼ばれているか試したところ、呼ばれていました。 しかし、spy上では呼ばれたことになっていません。ここで詰まりました。
なるべく英語は読みたくない(読めない)ので、 日本語の記事が無いか探してみたんですが、無さそうでした。
英語記事を漁っていると、StackOverFlowに
似た悩みを抱えた質問と解答が寄せられていました。
javascript – Backbone.js view tests using Sinon Spies in a browser – Stack Overflow
結論を先に書くと、先ほどのコード、惜しい感じでした。
間違っていたのは、spyの設定の仕方でした。
# 間違い
before ->
@view = new View( model: new Model() )
@spy = sinon.spy( @view, 'render' )
# 合ってる
before ->
@spy = sinon.spy( View.prototype, 'render' )
@view = new View( model: new Model() )
実行してみると、通ります。
このように、インスタンス化したオブジェクトにspyを忍び込ませるのではなく、
コンストラクタ関数のprototypeにspyを設定して、
その後にインスタンスを生成するとうまく動くようです。