sinonの使い方、使う場面(SpyとStub)

  • 10
    Like
  • 0
    Comment

始めに。

スタブとスパイの使い方について、イマイチ理解に悩んだのでメモ。
「使い方」というよりも、テストコードのどんな場面で使うべきか?って話。
結論から言うと、以下の使い分けのようだ。

  • スパイ(spy):実装済み/既存のオブジェクトの入出力を検証する用途
  • スタブ(stub):未実装、もしくは実行をフックしたいメソッドを代用する用途。

なお、モック(mock)は「目的が違う」のでこれら2つの同列に入らない認識(後述)。

前提条件

  • node.js v4.x。
    • でもrequireを<script>で代用すれば、ブラウザ環境にも適用可能。
  • npm install mocha chai sinon 済みの環境。
package.json
  "dependencies": {
    "chai": "^3.5.0",
    "mocha": "^3.0.2",
    "sinon": "^1.17.5"
  }

テスト対象のコード

以下のような、getAndshow() に対するテストを考える。

なお、テストフレームワーク(mocha等)を適用する場合は、このように「外部との相互作用」を受け持つ部分を差し替え可能な設計にしておく必要があるので注意。strategyパターンを適用すればOK。(必須とは思わないが、そうしておかないと、テストが面倒なことが多い)

sample_sinon.js
/*
    [sample_sinon.js]
    encoding="UTF-8"
*/

/**
 * @description パラメータに応じて情報を収集し出力する。
 * @param{object} data1 パラメータ1
 * @param{object} data2 パラメータ2
 * @param{object} strategyAssemble 情報収集を受け持つ外部メソッド
 * @param{objcet} outputInst 出力を受け持つ使う外部オブジェクト。
 * 
 * use case:
 * getAndshow( "http://fuga.com", "piyo", easyAssemble1, outputInst )
 */
function getAndshow( data1, data2, strategyAssemble, outputInst ){
    var item = {
        "url" : data1,
        "query" : data2 
    }
    var info = strategyAssemble( item, "workstyle" );

    outputInst.write( info );
};
exports.getAndshow = getAndshow;


/**
 * 期待した動作環境を整えるんが面倒なメソッドたち。
 */
function easyAssemble1( item, style ){
    return "外部環境に依存した値を返却:ケース1";
};
function easyAssemble2( item, style ){
    return "外部環境に依存した値を返却:ケース2";
};
exports.easyPattern = {
    "method1" : easyAssemble1,
    "method2" : easyAssemble2
};
sample_sinon_outputInst.js
/*
    sample_sinon_outputInst.js
    encoding="UTF-8"
*/

/**
 * 何らかの出力を行うオブジェクト。
 * write()メソッドを持つ。
 */
function OutputFunc(){
};
OutputFunc.prototype.write = function( data ){
    console.log( "[output]" );
    console.log( data );
};
exports.OutputFunc = OutputFunc;

テストコードにおける、sinonの使いどころ

上記のコードに対するテスト項目は、以下となる。

  • 引数 data1, date2 を元に構成したオブジェクトを以って strategyAssemble()を呼び出す。
  • そのとき、第二引数は規定の値を設定する(この例では:workstyle )。
  • strategyAssemble()が返したオブジェクトを以って、outputInst.write()を呼び出す。

(逆に言うと、上記が getAndshow() の設計に等しい。
 先にテストコードを書いて、後から実装する→「テストファースト」・・・と理解。)

上記のテスト項目をテストコードに起こすと、以下になる。

内部でoutputInst.write()を正しく呼んだか?を検証するために、write()をspyでラップしておく。
また、strategyAssemble() は外部環境に強く依存する動作であるとし、期待した外部環境をセットアップするのは手間なので、スタブで置き換える(stub out)。

sample_sinon_test.js
/*
    [sample_sinon_test.js]
    encodeing="UTF-8"
*/

var chai = require("chai");
var assert = chai.assert;
var expect = chai.expect;
var sinon = require("sinon");

var target = require("../src/sample_sinon.js");
var outputFunc = require("../src/sample_sinon_outputInst.js");


describe( "sample_sinon.js", function(){
    describe( "::getAndshow()", function(){
        it(" calls assemble() and write with outputInst()", function(){
            // テストの準備
            var inUrl = "http://dummy.com";
            var inQuery = "dummyQuery";
            var outputInst = new outputFunc.OutputFunc(); // テスト関数内で利用するインスタンスを生成
            var spyWrite = sinon.spy( outputInst, "write" ); // 呼び出しチェックするメソッドに「監視」を仕掛ける。
            var stubAssemble = sinon.stub(); // テスト関数内で利用するが、セットアップが面倒なのでスタブ化する(stub out:もみ消す)。
            var expectInfo = { "description" : "This is Dummy." }; 
            stubAssemble.onCall(0).returns( expectInfo ); // 疑似的な応答を設定する。

            // テストの実行
            target.getAndshow( inUrl, inQuery, stubAssemble, outputInst );

            // テスト結果の検証
            assert( stubAssemble.calledOnce, "assemble()を呼ぶ");
            expect( stubAssemble.getCall(0).args[0] ).to.deep.equal({
                "url" : inUrl, "query": inQuery
            }, "引数の1つめは、上記のオブジェクト。");
            expect( stubAssemble.getCall(0).args[1] ).to.equal( "workstyle",
            "引数の2つめは、文字列workstyleを指定。")

            assert( spyWrite.calledAfter( stubAssemble ), "続いて、write()を呼ぶ" );
            expect( spyWrite.getCall(0).args[0] ).to.deep.equal( expectInfo,
            "引数の1つめは、assemble()が返却したオブジェクト" );
        });
    });
});

これを実行すると、以下のようになる。
今回は既に「正しく実装済み」なので「成功」。

sinon_ok.png

たとえば、コードの実装のときに以下のようにウッカリ間違えていた、とする。
(似た名前の変数をどのプロパティに当てはめるとか、容易に間違えそうだ)

間違い)

    var item = {
        "query" : data1,
        "url"   : data2
    }

正しい)

    var item = {
        "url"   : data1,
        "query" : data2 
    }

上記のように誤ってコーディングしていた場合にテストを実行すると
以下のように失敗として、コーディングミスを検出出来る。

実行例:失敗
sinon_ng.png

spyとstub、 mockの違い。

ここでは、spyとstubを使っている。

  • Spy(スパイ):既存のメソッドの入出力を監視する。
    • 既に存在するメソッドをそのまま利用するときに、そのI/Oを検証するに使う。
    • スパイ化しても、本来のメソッドはそのまま呼び出される
  • Stub(スタブ):未実装か、もしくは既存だが動作環境を整えるのが面倒なメソッドを差し替える。
    • スタブ化した場合、本来のメソッドは呼び出されない
  • Mock(モック):上記2つとは用途が異なり、関数内の動作フローを検証したいときに使う。
    • 「Aという関数がxxで呼ばれてyyを返し、続いてBを~」という一本道を検証する目的で使う?
    • sinonの公式には「多くの場合、単一のテストでは2つ以上のモックを使うべきではない」とある。

When to not use mocks?

Mocks come with built-in expectations that may fail your test. Thus, they enforce implementation details. The rule of thumb is: if you wouldn’t add an assertion for some specific call, don’t mock it. Use a stub instead. In general you should never have more than one mock (possibly with several expectations) in a single test.

引用元:http://sinonjs.org/docs/#mocks

参考にしたWebサイト様

[Mocha, Chai, Sinon を使ったNode.js のテスト自動化 実践編 - Qiita]
http://qiita.com/TsutomuNakamura/items/0eb50bf7622a3906e101

[TDD _ モック _ スタブ - Qiita]
http://qiita.com/7of9/items/8e2cb2070f2b2ea4e5ec

[Mocha, chai, sinon で JavaScriptのテスト - Qiita]
http://qiita.com/sutetotanuki/items/ea60ab94ddb54b8d9288

[Sinon.JS - Documentation] ※Sinon.js 公式ページ
http://sinonjs.org/docs/