読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Sinon.JS を使った JavaScript のテスト

JavaScript mixi
初めましてこんにちは。ソーシャルクライアント開発の tanabe と申します。 今回は?Sinon.JS を使った JavaScript のテスト方法を紹介したいと思います。

Sinon.JS って何?

Sinon.JS はノルウェーのエンジニア Christian Johansen さんが書かれた、JavaScript 用のライブラリです。スタブやモック、フェイクオブジェクトの提供に特化していて、QUnit などのテスト用のフレームワークや実行環境に依存しない所が特徴です。Christian Johansen さんは?Test-Driven JavaScript Development の著者でもあり、こちらは近々翻訳版 が登場するようです。 では早速、Sinon.JS を使ったテスト手法をご紹介していきたいと思います。本稿ではテストフレームワークは QUnit を採用しています。

時間を扱うコードをテストしたい

例えば、宝の地図を出す際に『5 分経過後から 10 分経過する前に II コントローラのマイクに向かって叫ぶ』か『1 時間待つ』 ようなコードをテストする際に、1 時間もモニタの前でじっと待つのは不毛ですが、Sinon.JS の FakeTimer を使用すると時間をいとも簡単に進めることがきます。 下記の Game クラスには waitOneHour() を呼び出した後 1 時間後にようやく、canProceedNextStage() が true を返す仕様があります。
//1 時間待つ
Game.prototype.waitOneHour = function(date) {
    this.startDate = date;
};

//1 時間後に true を返す
Game.prototype.canProceedNextStage = function() {
    var now = new Date();
    if ((now.getTime() - this.startDate.getTime()) >= 1000 * 60 * 60 * 1 ) {
        return true;
    }
    return false;
};

次が、上記ゲームのテストコードです。
Sinon.JS の FakeTimer で擬似的な時計を作ることができます。擬似的な時計の tick() メソッドで時計の針を進ませると、Date インスタンスの値も連動して自動的に変わります。

//FakeTimer をセットアップ
module('date', {
    setup: function() {
        this.clock = sinon.useFakeTimers((new Date(2011, 0, 1)).getTime(),
            'setTimeout',
            'clearTimeout',
            'setInterval',
            'clearInterval',
        'Date');
    },

    teardown: function() {
        this.clock.restore();
    }
});

test('wait 1 hour', function() {
    var game = new Game();
    game.waitOneHour(new Date());
    //まだ次のステージに進むことができない
    notEqual(game.canProceedNextStage(), true);
    //1 時間進める
    this.clock.tick(1000 * 60 * 60 * 1);
    //次のステージに進むことができる
    ok(game.canProceedNextStage());
});

XMLHttpRequest を使うコードをテストしたい

次に、XMLHttpRequest (XHR) を Sinon.JS でテストする方法を紹介します。Sinon.JS の FakeXMLHttpRequest を使えば、エンドポイントが実際に存在しなくとも XHR のテストを行うことができます。

例えば、非同期通信でメッセージを取得するようなアプリケーションがあるとします。

var MessageReader = function() {};
MessageReader.prototype.getMessages = function(callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/get_message.pl');
    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            callback(this.responseText);
        }
    };
    xhr.send();
};

MessageReader の getMessages() をテストしたい場合、以下のようなテストコードになります。

既存の XMLHttpRequest インスタンスが FakeXMLHttpRequest に置き換わり、request.respond() で、任意のレスポンスを返すことができます。404 を返せば、通信が失敗した時のテストも容易に行えます。

// FakeXMLHttpRequest をセットアップ
module('xhr', {
    setup: function() {
        this.xhr = sinon.useFakeXMLHttpRequest();
        var requests = this.requests = [];
        this.xhr.onCreate = function (xhr) {
            requests.push(xhr);
        };
    },

    teardown: function() {
        this.xhr.restore();
    }
});

test('xhr request', function() {
    var messageReader = new MessageReader();
    messageReader.getMessages(function(response) {
        equals(response, '{result: "ok"}');
    });
    //成功時のレスポンスを返す
    this.requests[0].respond(200, {}, '{result: "ok"}');
});

関数が正しく呼ばれたかテストしたい

Sinon.JS には上記で紹介しました 2 種類のフェイクオブジェクト以外に、spy/stub/mock という 3 種類のオブジェクトが提供されています。これらは名前の通り、スタブやモックを作成したり、既存の関数をラップして関数の内容を変更する機能を持っています。中でも spy では、ある関数が呼ばれたかどうかや引数が正しく渡っているかなど、関数呼び出しに関するテストを行うことができます。

以下は spy を使ったテストコードです。テスト対象のコードはメールクライアントとします。

//メールクライアントのクラス
var Mailer = function() {};
Mailer.prototype.send = function(message) {
    //..メールを送信するコード
};

テストコードでは、Mailer インスタンスのメソッドを spy でラップして、正しく呼び出されているかどうかテストしています。

test('send mail', function() {
    var mailer = new Mailer();
    var spy = sinon.spy(mailer, 'send');
    mailer.send('hello');
    //呼び出された
    equals(spy.called, true);
    //引数が 'hello' で呼び出された
    equals(spy.calledWith('hello'), true);
});

エラーハンドリングをテストしたい

throw された例外の処理をテストする際には stub が役に立ちます。stub は spy と同じように既存の関数をラップして処理を上書きすることができます。throws() メソッドを呼び出すことで、任意に例外を投げられます。

例として先の Mailer を使用する User クラスを定義しました。

//メールクライアントを利用する User クラス
var User = function() {
    this.mailer = new Mailer();
};

User.prototype.sendMail = function(message) {
    try {
        this.mailer.send(message);
    } catch (error) {
        return false;
    }
    return true;
};

Mailer の send メソッドに引数が指定されなかった場合の User クラスの例外処理をテストしています。

test('send mail failure', function() {
    var user = new User();
    var stub = sinon.stub(user.mailer, 'send');
    //強制的に例外を投げる
    stub.throws();
    //戻り値が false となる
    notEqual(user.sendMail('hello'), true);
});

関数が呼び出された回数をテストしたい

関数が何回呼ばれたかなど、一連の処理の振る舞いをテストしたいときには mock を使用します。以下のコードでは 一度だけ呼ばれることを期待する exactly(1) を指定することで、個別に Mailer インスタンスを生成しても、User インスタンスが持つ Mailer インスタンスに影響が無いことをテストしています。

test('behaviour', function() {
    var user = new User();
    var otherMailer = new Mailer();
    var mock = sinon.mock(user.mailer);
    //一度だけ呼び出されたことを期待する
    mock.expects('send').exactly(1);
    user.sendMail('hello');
    //別の Mailer インスタンスが send() を実行
    otherMailer.send('hello hello how low?');
    //一度だけ呼び出されたことが保証された
    ok(mock.verify());
});

駆け足になりましたが、以上が Sinon.JS を使った JavaScript のテスト手法の紹介となります。FakeTimer や FakeXMLHttpRequest など、かゆいところに手が届くライブラリだと思いますのでJavaScript の単体テストのお供にいかがでしょうか。

ちなみに Sinon は「シノン (シノーン)」と読み、トロイ戦争で活躍したスパイの名前だそうです。