Backbone.js×sinon.jsのテストでspyが上手く動かない時のメモ

こんにちは。
最近、Backbone.jsというライブラリを使って、制作をしています。

Backboneいいですね〜。
各UIパーツの結合度が下がるので、
全体の見通しが良くなり、メンテもしやすくなります。

今作っているものはそこまで規模が大きいものではないのですが、
大規模js開発入門ということで。

それに加えて、先日JavaScript道場に行ってきてから、
jsの開発でもテストコードを書くようにしています。

師範に習ったとおり、
mocha + expectjs + sinonjsを用いてユニットテストを書いているのですが、
そのテストを書いている時に、sinonjsのspyで詰まったのでメモ。

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()

実行してみると、通りません。。

Moch

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() )

実行してみると、通ります。

Moch

このように、インスタンス化したオブジェクトにspyを忍び込ませるのではなく、
コンストラクタ関数のprototypeにspyを設定して、
その後にインスタンスを生成するとうまく動くようです。