Fork me on GitHub

テストを補助する機能について

See also

busterjs-kumite/test-patterns at develop · azu/busterjs-kumite · GitHub
cd `git rev-parse --show-toplevel`/test-patterns/ サンプルコード

setup/teardown

setUp/tearDownはそれぞれのテストの 実行前(setup)/実行後(teardown) に実行する関数を登録できます。 多くのテストフレームワークで同様の機能が提供されています。

/busterjs-kumite/test-patterns/test/setup-teardown.js

buster.testCase("setup/teardown", {
    setUp : function(){
        this.i = 1
    },
    tearDown : function(){
        delete this.i;
    },
    "one" : function(){
        this.i = 10;
        assert.equals(this.i, 10);
    },
    "two" : function(){
        assert.equals(this.i, 1);
    }
});

上記の例では、 "one""two" それぞれの前後でsetUp/tearDownが実行されています。

テスト内で共通のプロパティを使いたい場合は、 this にプロパティを追加して利用できます。 ここでは、 this.i を実行前に値を設定して、実行後に削除しています。 そのため、 "one" の中で this.i の値を変更しても、 "two" には影響がでないようになっています。

Test case contexts

Buster.JSでは一つのtestCase内にコンテキストを複数持つことができます。

/busterjs-kumite/test-patterns/test/testcase-context.js

buster.testCase("testCase Context", {
    "test #1" : function(){
        assert.same("test", "test");
    },
    "context" : {
        "test #2" : function(){
            assert(true);
        },
        "Nest Context" : {
            "test #3" : function(){
                refute(false);
            }
        }
    }
});

上記のコードだと、test #1が所属するコンテキスト、test #2が所属するのコンテキスト、test #3が所属するの3つがあることが分かります。 コンテキストにまとめることで、一つのtestCase内でもテストを一定のグループにまとめることができるようになっています。

コンテキストにはそれぞれ、setUp/tearDownを書くことができ、そこで書いたsetUp/tearDownはそのコンテキスト以下のテスト時に使われます。

また、 Test reporters によってはコンテキストを認識して見やすい形で出力してくれる場合もあります。

// Deferred tests

Deferred testsとはDeferredを使ったテストという意味ではなくて、そのテストそのものを延期することを示しています。 延期されたテストは、テスト実行時に実行されない用になります。

テスト名を//から始める事でそのテストは Deferred test になります。

/busterjs-kumite/test-patterns/test/deferred-test.js

buster.testCase("Deferred", {
    "// test isn't implement" : function(){
    },
    context : {
        setUp : function(){
            buster.log("doesn't call");
        },
        "// this context all Deferred" : function(){
        }
    }
});

実行するとDeferred された有無も表示されます。 コンソールの場合は、 Deferred - Travis CI のような感じで出力されます。

また、 Test case contexts などで、そのコンテキストにDeferred testsのみしかない場合は、 そのコンテキストに存在するsetUp/tearDownは実行されないようになっています。

そのため、上記の例では buster.log("doesn't call"); は呼ばれない事になります。

非同期テスト

Buster.JSでは非同期のテストもサポートされている。 非同期テストの手法は大きく分けて3種類用意されている

  • Callback done flag - 引数doneの実行を終了フラグとする
  • Promise - Promiseオブジェクトをreturnする
  • 非同期をMock/Stub/Fakeなどを使い同期的にする

Callback done flag

まずは、一番最初のケースについて。

下記のように、通常のテストにsetTimeout(非同期の動作)を混ぜてしまうとがsetTimeoutの中身を実行する前にテストが終了してしまい、 assertが一つもないテストという扱いになってしまいます。

"test not asynchronous" : function(){
    setTimeout(function(){
        assert(true);
    }, 100);
}
// ✖ My thing test not asynchronous
// No assertions!

これを回避するためには、テストに指定する関数に引数を受け取るようにします。

その引数のコールバック(下記ではdone)が実行されるまで、テストは終了されないようになるため、非同期の動作を含んだテストを実行することができます。

/busterjs-kumite/test-patterns/test/async-test.js

buster.testCase("asynchronous", {
    "test asynchronous" : function(done){
        setTimeout(function(){
            assert(true);
            done();// test end
        }, 100);
    }
});

Promise

Promiseとは何かについては下記を参照して下さい。

CommonJSのPromiseに沿ったシンプルなオレオレDeferred書いた - Cheese Pie より:

Promiseは"未完"な状態から始まり、"未完"あるいは"完了"あるいは"失敗"の状態へ遷移すること
Promiseは"then"という名前の関数をもったオブジェクトを返すこと
"then"メソッドはPromiseを返しチェインできるようにし、またコールバックでエラーが起きた場合は"失敗"の状態へ遷移すること

Buster.JSではPromiseを使ったテストをサポートしていて、テストでpromiseオブジェクトを返すと、 そのpromiseがresolveするまでテストの終了を待ってくれます。

これは Callback done flag での引数のコールバックdoneが実行されるまでテストが終了されないのと似ています。

実際のPromiseを使った例を見てみましょう。

Promiseは then メソッドを持ったオブジェクトであればいいので、特別なライブラリを使わなくても手動で実現することもできます。 下記の例では手動で実現するパターンと、Buster.JSに含まれているPromiseの実装である、 when.js を 使った2つの例が入っています。

/busterjs-kumite/test-patterns/test/promise-test.js

buster.testCase("Promise", {
    "test async, use promise" : function(){
        var promise = { // then メソッドの有無が重要
            then : function(callback){
                this.callbacks = this.callbacks || [];
                this.callbacks.push(callback);
            }
        };

        setTimeout(function(){
            buster.assert(true);
            var callbacks = promise.callbacks || [];
            for (var i = 0, l = callbacks.length; i < l; ++i){
                callbacks[i]();
            }
        }, 100);
        // thenを持ったものを返す => promiseと判断 (isPromise)
        return promise;
    },
    "test async ,use when.js" : function(){
        var deferred = when.defer();

        setTimeout(function(){
            buster.assert(true);
            deferred.resolver.resolve();
        }, 100);

        return deferred.promise;
    }
});

どちらも、基本的に同じ事をやっていて、テスト関数内でreturnで then メソッドを持ったPromiseオブジェクトを返してあげる。 そのPromiseオブジェクトのresolveをする前に、テストしたい内容をassertしてあげて、resolveするとテストが終了します。

基本的にはwhen.jsを使った方がシンプルな記述で済みます。

Mock/Stub/Spy/Fake

Mock/Stub/Fakeを使って非同期のコードを同期的にテストしたり、 依存関係などを緩和してテストを実行したり、テストを楽にしてくれます。

Sinon.JS

Todo

@ryuoneさん、@kyo_ago さんがもっとくわしく書いてくれる

Buster.JSには Sinon.JS が統合されています。 Sinon.JS はをMock/Stub/Spy/Fake Timer/Fake XHRなどのテストを補助する機能が含まれています。

以下に、Sinon.JSの主要な機能を簡単に使ったテストを書いてみます。

/busterjs-kumite/test-patterns/test/sinon-test.js

buster.testCase("SinonJS", {
    setUp : function(){
        this.myLib = {
            method : function(str){
                return str;
            }
        }
    },
    "test a spy" : function(){
        var spy = this.spy(this.myLib, "method");// sinon.spy
        this.myLib.method("test");
        assert.calledOnce(spy);
    },
    "test a stub" : function(){
        var stub = this.stub(this.myLib, "method");// sinon.stub
        stub.withArgs("hello").returns("world");// "hello" を渡すと "world" を返す
        assert.equals(this.myLib.method("hello"), "world");// return "world"
    },
    "test a mock" : function(){
        var mock = this.mock(this.myLib);// sinon.stub
        mock.expects("method").withArgs("hello");// 期待値を設定
        this.myLib.method("hello");
        assert(mock);
    },
    "test async in sandbox" : {
        setUp : function(){
            // SandboxにFakeを作る
            this.sandbox.useFakeServer();
            this.sandbox.useFakeTimers();
        },
        tearDown : function(){
            // Sandboxを戻す
            this.sandbox.restore();
        },

        "test FakeTimer" : function(){
            var spy = this.spy();
            setTimeout(function(){
                spy();
            }, 16);
            this.sandbox.clock.tick(16);
            assert.called(spy);
        },
        "test FakeServer" : function(){
            var expectRes = {
                status : "OK"
            };
            var xhr = new XMLHttpRequest();
            xhr.open('GET', '/get', true);
            xhr.onreadystatechange = function(){
                if (this.readyState === XMLHttpRequest.DONE && this.status === 200){
                    var res = JSON.parse(this.responseText);
                    assert.match(res, expectRes);
                }
            };
            xhr.send();
            this.sandbox.server.requests[0].respond(200, {}, JSON.stringify(expectRes));
        }
    }
});

コードを見てみると、 Sinon.JSを読みこまなくても this.spy,this.mock,this.sandbox などで参照できていることが分かります。 また、 assert.calledOnce のように、いくつかSinon.JSを前提とされているようなassertionが存在してることが分かります。

このように、Buster.JSとSinon.JSでは作者が同じ事もあって、Sinon.JSを使ったテストが多少楽に出来るようになっています。