JavaScriptのテストダブル、定番Sinon.jsと新星testdouble.jsどっちを選ぶ?

2017/06/21

Jani Hartikainen

79

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

関数が呼び出されたかどうやって確認しますか? Ajaxコールのテスト方法は? setTimeoutを使ったコードは?テストで困ったときに使えるのがテストダブルです。

テストダブルは、難しいテストを簡単に実行できる代替コードです。

これまではSinon.jsがJavaScriptのテストダブルを作成するには不可欠なツールでしたが、最近、testdouble.jsという新しいライブラリーが話題になっています。Sinon.jsと同様の機能を備えていますが、いくつか違いがあります。

この記事では、Sinon.jsとtestdouble.jsのできることを紹介し、長所と短所を比較します。Sinon.jsがデファクトスタンダードのままになるか、それとも挑戦者がその座を勝ち取るのでしょうか?

注意:テストダブルをよく知らない人は、まず私が書いたSinon.jsチュートリアルを読むことをおすすめします。この記事で紹介する用語の理解を深めるのに役立ちます。

この記事で使用する用語の説明

この記事を理解しやすくするために、使用する用語を簡単に紹介します。以下はSinon.jsの定義です。ほかで使われるときは、若干意味が変わることがあります。

  • テストダブルは、テスト時に使用する機能を代替するものです。以下の3つのタイプに分類されます。
  • スパイは、ターゲット関数に影響を与えずに、効果の検証をするテストダブルです。
  • スタブは、ターゲット関数の動作を返り値など別のもので代替するテストダブルです。
  • モックはスタブとは異なるアプローチです。モックには検証機能が含まれていて、個別のアサーションの代わりに使用できます。

testdouble.jsの目的の1つは、これらの用語の混乱を減らすことです。

Sinon.jsとtestdouble.jsの概要

Sinon.jsとtestdouble.jsの基本的な使い方を比較します。

Sinonには、スパイ、スタブ、モックと異なるテストダブルがあります。それぞれに違った使い方があり、ほかの言語を使っている人や、xUnit Test Patternsをはじめ、同じ用語のライブラリーを使ったことがある人にとってなじみやすくなっています。一方で、Sinonをはじめて使う人には難しく感じます。

以下がSinonの使い方の基本的な例です。

//Here's how we can see a function call's parameters:
var spy = sinon.spy(Math, 'abs');

Math.abs(-10);

console.log(spy.firstCall.args); //output: [ -10 ]
spy.restore();

//Here's how we can control what a function does:
var stub = sinon.stub(document, 'createElement');
stub.returns('not an html element');

var x = document.createElement('div');

console.log(x); //output: 'not an html element'
stub.restore();

対照的に、testdouble.jsはより簡単なAPIを提供しています。スパイやスタブは使わずに、td.functiontd.object、td.replaceなどのJavaScriptの開発者が慣れ親しんでいる用語を使います。これがtestdoubleが選ばれる理由の1つです。testdoubleは特定のタスクを実行するのにも適しています。一方で、上級者からは選ばれにくくなっているかもしれません(これは意図的な部分もあります)。

以下がtestdouble.jsの使用例です。

//Here's how we can see a function call's parameters:
var abs = td.replace(Math, 'abs');

Math.abs(-10);

var explanation = td.explain(abs);
console.log(explanation.calls[0].args); //output: [ -10 ]

//Here's how we can control what a function does:
var createElement = td.replace(document, 'createElement');
td.when(createElement(td.matchers.anything())).thenReturn('not an html element');

var x = document.createElement('div');
console.log(x); //output: 'not an html element'

//testdouble resets all testdoubles with one call, no need for separate cleanup
td.reset();

testdoubleはより簡単です。関数を「スタブする」のではなく「置き換え」ます。関数から情報を得るために、testdoubleに「説明」を要求します。この点以外は、Sinonに似ています。

「アノニマス」のテストダブルも作れます。

Sinon.js
var x = sinon.stub();
testdouble.js
var x = td.function();

Sinonのスパイとスタブには、stub.callCountstub.argsなど多くの情報を提供するプロパティがあります。testdoubleは、td.explainで情報を取得します。

//we can give a name to our test doubles as well
var x = td.function('hello');

x('foo', 'bar');

td.explain(x);
console.log(x);
/* Output:
{
  name: 'hello',
  callCount: 1,
  calls: [ { args: ['foo', 'bar'], context: undefined } ],
  description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n  - called with `("foo", "bar")`.',
  isTestDouble: true
}
*/

スタブと検証の方法が大きく異なります。Sinonは、スタブのあとでコマンドを連鎖させ、アサーションを使って結果を検証します。testdouble.jsはシンプルで、関数を呼び出す方法、もしくは呼び出しをテストする方法が見るだけで分かります。

Sinon.js
var x = sinon.stub();
x.withArgs('hello', 'world').returns(true);

var y = sinon.stub();
sinon.assert.calledWith(y, 'foo', 'bar');
testdouble.js
var x = td.function();
td.when(x('hello', 'world')).thenReturn(true);

var y = td.function();
td.verify(y('foo', 'bar'));

どの操作をいつ連鎖できるか知る必要がないため、testdoubleのAPIは理解しやすくなっています。

一般的なテスト作業の詳細な比較

2つのライブラリーは似ています。しかし、実際のプロジェクトのテスト作業ではどうでしょうか? 違いが出てくるケースを紹介します。

testdouble.jsにはスパイがない

まず注意すべきことは、testdouble.jsには「スパイ」がないことです。Sinon.jsは、関数の呼び出しを置き換えて、関数のデフォルトの動作を維持しながら情報を取得できますが、testdouble.jsは、関数をtestdoubleで置き換えると、デフォルトの動作を失います。

しかし、さして問題ではありません。スパイの一般的な用途である「コールバックが呼び出されたか確認する」のは、td.functionで簡単に実行できます。

Sinon.js
var spy = sinon.spy();
myAsyncFunction(spy);

sinon.assert.calledOnce(spy);
testdouble.js
var spy = td.function();
myAsyncFunction(spy);

td.verify(spy());

testdouble.jsでスパイをより深く使えるのではと期待してショックを受けないためにも、この違いを理解するのは良いことです。

testdouble.jsはより正確な入力が必要になる

2つ目の違いは、testdoubleは入力のチェックが厳しいことです。

Sinonのスタブとアサーションは、与えるパラメータを正確に定義しなくても大丈夫です。分かりやすい例を挙げます。

Sinon.js
var stub = sinon.stub();
stub.withArgs('hello').returns('foo');

console.log(stub('hello','world'));//output: 'foo'

sinon.assert.calledWith(stub,'hello');//no error
testdouble.js
var stub = td.function();
td.when(stub('hello')).thenReturn('foo');

console.log(stub('hello','world')); //output: undefined

td.verify(stub('hello')); //throws error!

基本的にSinonは余分なパラメータが関数に付与されても問題ありません。提供している関数「sinon.assert.calledWithExactly」はドキュメントでデフォルトとして提案されていませんし、関数「stub.withArgs」は正確なバリアント型を取りません。

一方、testdouble.jsは基本的に正確なパラメータを指定する必要があります。指定されていないパラメータが関数に与えられていたら、バグの可能性があるためテストを失敗させるべき、という考えが基になっています。

testdouble.jsに任意のパラメータを指定することは可能ですが、デフォルトではありません。

//tell td to ignore extra arguments entirely
td.when(stub('hello'),{ ignoreExtraArgs:true}).thenReturn('foo');

ignoreExtraArgs: trueを使ったときの反応はSinon.jsに似ています。

testdouble.jsにはPromiseのサポートが組み込まれています

Sinon.jsで使うPromiseは複雑ではありません。testdouble.jsにはPromiseを返したり却下したりする組み込みメソッドがあります。

Sinon.js
var stub = sinon.stub();
stub.returns(Promise.resolve('foo'));
//or
stub.returns(Promise.reject('foo'));
testdouble.js
var stub = td.function();
td.when(stub()).thenResolve('foo');
//or
td.when(stub()).thenReject('foo');

注意:sinon-as-promisedを使用してSinon 1.xに同様の便利機能を含めることができます。Sinon 2.0以降には、stub.resolvesとstub.rejectsという形でPromiseのサポートが含まれています。

testdouble.jsのコールバックのサポートはより強力です

Sinonとtestdoubleどちらもコールバックを呼び出すスタブ関数を簡単に作れます。しかし、動作の仕方が異なります。

Sinonはstub.yieldsを使って、スタブがパラメーターを受け取る最初の関数を呼び出します。

var x = sinon.stub();
x.yields('a','b');
//callback1 is called with 'a' and 'b'x(callback1, callback2);

testdouble.jsのデフォルトはnodeスタイルのパターンで、コールバックは最後のパラメータとみなされます。また、呼び出しをテストするときに指定する必要はありません。

var x = td.function();
td.when(x(td.matchers.anything())).thenCallback('a','b');
//callback2 is called with 'a' and 'b'x(callback1, callback2);

複数のコールバックを持つシナリオやコールバックの順序が異なるシナリオの反応を簡単に定義できることが、testdoubleのコールバックサポートをより強力にします。

代わりにcallback1を呼び出します。

var x = td.function();
td.when(x(td.callback, td.matchers.anything())).thenCallback('a','b');
//callback1 is called with 'a' and 'b'x(callback1, callback2);

td.callbacktd.whenの関数に最初のパラメータとして渡したことに注目してください。testdoubleに使いたいコールバックを伝えています。

Sinonでは、反応を変えることも可能です。

var x = sinon.stub();
x.callsArgWith(1,'a','b');
//callback1 is called with 'a' and 'b'x(callback1, callback2);

この場合、yieldsではなくcallsArgWithを使います。機能させるために呼び出し用のインデックスを与える必要があります。これは、特に多くのパラメータを持つ関数で多少問題になる可能性があります。

両方のコールバックを、いくつかの値で呼び出す場合はどうなるでしょう?

var x = td.function();
td.when(x(td.callback('a','b'), td.callback('foo','bar'))).thenReturn();

//callback1 is called with 'a' and 'b'
//callback2 is called with 'foo' and 'bar'
x(callback1, callback2);

Sinonでは不可能なのです。callsArgWithへ複数の呼び出しを連鎖できますが、そのうちの1つを呼び出すだけです。

testdouble.jsにはモジュール置き換え機能が組み込まれています

testdoubleは、td.replaceを使って関数を置き換えることに加え、モジュール全体を置き換えることができます。

これは主に置き換える必要のある関数を、直接出力するモジュールがある場合に役に立ちます。

module.exports = function() {
  //do something
};

これをtestdoubleで置き換えたい場合は、td.replace('path/to/file')が使えます。

var td =require('testdouble');
//assuming the above function is in ../src/myFunc.jsvar myFunc = td.replace('../src/myFunc');
myFunc();

td.verify(myFunc());

Sinon.jsはオブジェクトのメンバーである関数は置き換えられますが、同様の方法ではモジュールの置き換えはできません。Sinonで実行するには、proxyquirerewireなど別のモジュールを使います。

var sinon = require('sinon');
var proxyquire = require('proxyquire');
var myFunc = proxyquire('../src/myFunc', sinon.stub());

モジュールの置き換えでもう1つ知っておきたいことは、testdouble.jsがモジュール全体を自動的に置き換えることです。上記の関数の出力であれば関数を置き換えます。複数の関数を含むオブジェクトであれば、すべて置き換えます。コンストラクタ関数とES6のクラスもサポートされています。proxyquireとrewireのどちらも、置き換えるものとその方法を個別に指定する必要があります。

testdouble.jsにはSinonのヘルパーの一部がない

Sinonの疑似タイマー疑似XMLHttpRequest、疑似サーバーを使っているなら、testdoubleにそれらがないと気づくでしょう。

疑似タイマーはプラグインで利用できますが、XMLHttpRequestsとAjaxは別の方法で実現する必要があります。

簡単な解決策は、使用している$.postなどのAjax関数を置き換えることです。

//replace $.post so when it gets called with 'some/url',
//it will call its callback with variable `someData`
td.replace($, 'post');
td.when($.post('some/url')).thenCallback(someData);

testdouble.jsはテスト後のクリーンアップが簡単

Sinon.jsの初心者がよくぶつかる障壁は、スパイやスタブのクリーンアップです。Sinonは3つの方法を提供していますが、役に立ちません。

1つめ
it('should test something...', function() {
  var stub = sinon.stub(console, 'log');
  stub.restore();
});
2つめ

describe('something', function() {
  var sandbox;
  beforeEach(function() {
    sandbox = sinon.sandbox.create();
  });

  afterEach(function() {
    sandbox.restore();
  });

  it('should test something...', function() {
    var stub = sandbox.stub(console, 'log');
  });
});
3つめ
it('should test something...', sinon.test(function() {
  this.stub(console, 'log');

  //with sinon.test, the stub cleans up automatically
}));

誤ってスタブやスパイを残してしまい、ほかのテストで問題を引き起こす可能性があるため、実際にはsandboxとsinon.testメソッドが推奨されています。その結果、追跡するのが困難な連鎖的な障害が起こってしまいます。

testdouble.jsは、テストダブルをクリーンアップする方法として、td.reset()を提供しています。推奨される方法は、afterEachをフックに呼び出すことです。

describe('something', function() {
  afterEach(function() {
    td.reset();
  });

  it('should test something...', function() {
    td.replace(console, 'log');

    //the replaced log function gets cleaned up in afterEach
  });
});

これにより、テストダブルのセットアップとテスト後のクリーンアップが簡単になり、バグの追跡が困難になる可能性が少なくなります。

長所と短所

2つのライブラリーの機能を紹介しました。同様の機能を提供していますが、設計思想が多少異なります。これを長所と短所に分解できるでしょうか?

Sinon.jsはtestdouble.jsに比べ機能が豊富で、設定可能な項目も多くあります。そのため、特殊なテストシナリオに柔軟に対応できます。また、スパイ、スタブ、モックなどさまざまなライブラリーに存在し、テスト関連の書籍でもよく出てくる用語を使っているため、ほかの言語を使っている人にも親しみやすくなっています。

欠点は複雑さです。エキスパートな人には、柔軟性はより多くのことができますが、作業によってはtestdouble.jsよりも複雑になります。テストダブルはじめて学んだ人の中にもすぐに覚えられる人もいるでしょうが、私のように精通している人でさえ、sinon.stubとsinon.mockの違いを説明するのに苦労することもあります。

testdouble.jsは、インターフェイスが比較的シンプルです。Sinon.jsは、ほかの言語を念頭に置いて設計されていると感じることがありますが、testdouble.jsのインターフェイスは簡単で、直感的にJavaScript向けだと分かります。おかげで、初心者が簡単に始めやすく、経験豊富なエンジニアでさえ、多くのタスクを簡単にできるようになります。たとえば、testdoubleはテストダブルを設定するときと結果を検証するときに同じAPIを使います。また、クリーンアップメカニズムがシンプルなので、エラーが発生しにくい長所もあります。

testdoubleの最大の短所は、たとえば「全体のスパイが足りない」とスタブの代わりにスパイを使いたいのに使えないなど、設計思想そのものにあります。よく言われるのですが、問題を見つけることが難しい点が挙げられます。とはいえ、testdouble.jsは新参者にもかかわらず、古参のSinon.jsと十分に渡り合える部分があります。

機能比較

以下が機能の比較です。

機能 Sinon.js testdouble.js
スパイ あり なし
スタブ あり あり
スタブの結果を遅らせる なし あり
モック あり あり ※1
Promiseのサポート あり(2.0以降) あり
タイムヘルパー あり あり(プラグイン経由)
Ajaxヘルパー あり なし(代わりにReplace機能がある)
モジュール置き換え なし あり
組み込みアサーション あり あり
Matcher あり あり
カスタムMatcher あり あり
引数キャプチャ なし ※2 あり
プロキシテストダブル なし あり

※1.testdouble.jsはSinon.jsのようなモックがありません。しかし、Sinonのモックはスタブと検証を含むオブジェクトなので、td.replace(someObject)で同様の効果を得られます。
※2.stub.yield(stub.yieldsと混同しないでください)で引数キャプチャと同様の効果が得られます。

まとめと結論

Sinon.jsとtestdouble.jsは、類似した機能を提供しています。どちらかが一方的に優れているわけではありません。

最大の違いはAPIにあります。Sinon.jsは少し冗長ですが多くのオプションを提供しています。これは良い面でも悪い面でもあります。testdouble.jsはより合理化されたAPIで、学習しやすく使い勝手もよいですが、設計思想が強いために問題が生じる場合があります。

結局、どちらが良いのか

testdoubleの設計思想に準じるなら、使わない理由はありません。私はSinon.jsを多くのプロジェクトで使いましたが、testdouble.jsでもSinon.jsで実施したことの95%以上を確実に代替できて、おそらく残っている5%も簡単な回避策で実現可能です。

Sinon.jsが使いにくいと思ったり、テストダブルのためにもっと「JavaScripty」な方法を探したりしているなら、testdouble.jsがぴったりです。すでに時間をかけてSinonを学んだとしても、testdouble.jsを試すことをおすすめします。

しかし、testdouble.jsの一部が、Sinon.jsを知っている人やベテランのテスターを悩ませるかもしれません。たとえば、全体のスパイ不足によってテストが壊れることもあります。エキスパートや柔軟にダブルテストをしたい人には、依然としてSinon.jsは優れた選択肢です。

テストダブルの使い方をもっと学びたいなら、私のSinon.js実践ガイドを見てみてください。無料です。このガイドではSinon.jsを使っていますが、testdouble.jsでも同じ手法やベストプラクティスを適用できます。

本記事はJames WrightJoan YinChristian JohansenJustin Searlsが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:JavaScript Testing Tool Showdown: Sinon.js vs testdouble.js

[翻訳:萩原伸悟/編集:Livit

Copyright © 2017, Jani Hartikainen All Rights Reserved.

Jani Hartikainen

Jani Hartikainen

15年以上、あらゆるJavascriptのアプリ構築に携わってきました。ブログではJavaScript開発者たちに不正なコードを排除する方法を教え、素晴らしいアプリの作成に集中し、問題を解決できるようなノウハウを提供しています。

Loading...