lxyuma BLOG

開発関係のメモ

jsでTDD!MochaとChaiとsinon.js入門

※この記事は社内勉強会向けの資料の下書きです。書きなぐりの下書きで見直すと最後の方の文書がヤバいので、いつか書き直します。読み辛い所は申し訳ないです。

概要

  • TDD
    • テスト自動化とTDDを整理
    • TDDとBDDの違い
  • Test Framework in javascript
    • QUnit/jasmine/mochaについて、違いやメリデメを知る
  • mocha
  • chai
    • 記述形式の違い整理
    • 基本文法
  • sinonjs
    • spy
    • stubs
    • mock

TDD

Test Driven Development

特徴

  • xUnit系/BDD系のテストフレームワーク使う
    • テストするコードも実装
  • テストファースト
    • 実装の後にテストするのではなく、テストを先に書いて実装する
  • サイクル
    • Red(失敗) => Green(通過) => Refactoring
    • 仮実装 => 三角測量 => 明白な実装

よくある勘違い

  • TDDはテスト手法...ではない
    • これを真面目に考えて、通常のテスト観点でテストすると、膨大な量のcodeを書く事になり、TDDがコスト要因になる。よくある失敗例。
    • また「何を書いていいのか分からない」状態になる。これもよくある失敗例。test firstで、通常のテスト観点は書けない。

それでは、TDDとは?

  • TDD は結果としてテストをしているだけ。
    • TDDの本質は、設計や分析
    • あるコンポーネントのインタフェースや構造を整理していく為の方法
    • 多くの人が誤解するので、整理する為にBDDなんて言葉も生まれた(後述)

kent beck says

  • 実は、TDDの話の大元のKent Beckさんも、著書の中で、さらっと大事な事を言ってる
TDDの皮肉の1つは、TDDがテスト技法ではない(カニンガム考案)ことである。
TDDは分析技法および設計技法であり、実際には開発のすべてのアクティビティを
構造化するための技法である。

(Kent Beck テスト駆動開発入門 P199)

TDDとテスト自動化

  • 通常のテスト観点で一生懸命テスト自動化していく人達もいる。
    • これらも間違えではない
    • ただ、元々のTDDの話とはまた違う話。
      • (違う話だが、この事をTDDと言う人もいる)
  • ここに2つの話がある点に注意する必要がある。
    • テストを自動化する話 = テストの話
    • TDDの話 = 設計の話(結果としてテストが残る)
  • 注意すべきは、test firstの話。これはTDDの話(設計の話)であって、テストの話ではない。(=> test firstでテスト観点でテスト書けない)
  • ややこしい事に、現実、実際の現場のソースも両者ごっちゃで書いて行く事になると思う。
    • テスト自動化は多くの場合(全てではないが)コストが高い。自動化のメリットがあるのか良く考えるべき

TDDとBDDの違い

  • TDDからBDDへ
    • TDDは冒頭で話した通り、テストと勘違いすると破綻する
    • TDDは設計を意識するべきで、この誤解を解くためTDDのT(テスト)をB(Behaiver:振る舞い)に変えたものがBDD
  • BDD
    • TDDと比べて、書くべきソースに差はない
    • 但し、上述の誤解の無いように、ツール側でソースやメッセージの中に振る舞いを前面に出して書けるようにしている。
    • なお、ここで話しているのは、狭義な意味でのBDD。これは、TDDと同じレベルの話だが、ちなみに、広義な意味でのBDDの話もある。広義な意味でのBDDは、cucumberのようにstory形式で書くATDDの話とrspecのようにspecification形式で書くTDDの話と2つに分かれる。ここでは、TDDと比べるだけなので、狭義な意味のBDDしか扱わない。
  • BDD後のTDD
    • BDD登場後、TDD系のxUnit系ツールもBDDっぽく改良されつつある
    • 振る舞い書く/ネスト出来る/実際の値と期待値の順番が自然言語と同じ等、BDDの特徴だった機能もTDD系のツールにも入ってきた。
    • 従って、特に後発のTDD/BDDツールの機能の差は殆どない。言葉が違うだけ。mochaも同じ。

jsのUT周辺のTestFrameworks

ここ数年で主流が、コロコロ変わってきた。

  • 数年前:QUnit
    • jQuery系の人はまだQUnit多い。
    • 例えば、jQuery使ってるBackboneのplugin系も殆どQUnit(Backbone本体がQUnitなので、まとめてテストする為にQUnit使ってるんでしょうね)
  • 去年くらいまで:jasmine
    • rails系とangular系はまだjasmine多い印象(angularはオリジナルなのだが)
    • にしても、以前と比べて見なくなってきた。
  • 今年あたり:mocha周りが熱かった

    • アサーションライブラリは、chai/expect.js/should.jsを筆頭に幾つか分かれる
    • mocha自体、とても汎用的(TDDもBDDもいけるの)で、拡張性も高いので、もうそろそろこの辺りで落ち着く?
  • ちなみに、DailyJSの2013surveyによると、まだjasmineが1位だった

    • Jasmineが30%
    • Mochaが27%
    • QUnit 16%

どれを使う?

  • 最近の新しいprojectで多いのは?
    • やっぱりmochaが多い気がする
  • 機能的には?
    • mochaの非同期の簡潔さは大きい
    • mocha自体は汎用的なので、どこまでもライブラリ追加して強力なツールに出来る。
    • jasmineは一通り必要な物は揃ってる(All in One package)
    • QUnitは大分機能が少ない(逆に覚える事も少ない)
  • レガシーIEでtestできる?
    • mocha使う = chai使う = IEは9以上をsupport(後述するが、形式次第ではie9でも動かず)
    • jasmineは公式情報で明示はないが、辿って行くとie6/7/8から動く様子
  • 学習コストは?
    • QUnit < jasmine < mocha の順に学習量は多くなる。

TestFWを整理

  • 流行りと非機能等機能性 => mocha
  • 気軽に始めたい、レガシーIEもテストしたい => jasmine
  • とにかくsimpleにしたい、jQuery系plugin開発等 => QUnit

あまりこだわりなければ、とりえあず、今はmochaで。


mocha

Mochaはnodeやブラウザ上で動かすjsのtest framework

  • nodeやブラウザでjsを実行可能
  • レポート出力も可能
  • 遅いテストの検出等のテスト支援機能ある
  • アサーションやテストダブルライブラリは別途準備が必要

メリデメ

  • メリット
    • styleを選べる(TDD or BDD/ mockライブラリも好きな物使える)
    • 非同期のコードが綺麗
  • デメリット
    • 自分で別途ライブラリ組み合わせる必要あるという手間。
    • しかも、今後、その別途ライブラリも廃り流行でてくるんだろうな(これは、特にjs周辺は仕方ないか)

アサーションが別ってどういう事?

そもそもアサーションとは

  • そもそもアサーションとは、以下の例で言うと、検証部分の事(should.equal)
   [1,2,3].indexOf(5).should.equal(-1);
  • ここを別のライブラリに頼ってる
    • ライブラリ毎に記述が違う
    • = 記述を選べる

アサーションの選択肢

  • 選択肢(の中から代表的な物)

    • should.js:rspec(BDD)みたい
      • user.should.have.property('name', 'tj');
    • expect.js:jasmine(BDD)みたい
      • expect(window.r).to.be(undefined);
    • chai:柔軟に書ける(BDD/TDD)
      • 上記、sholdやexpectいずれも使える。他に、xUnit(TDD)みたいなassert.equal(foo, 'bar');も書ける
  • 参考データ(2013/11/30現在の①github star数と②github更新状況)

    • should.js
      • ①1241 ☆
      • ②昨日
    • expect.js
      • ①524 ☆
      • ②半年前(更新停滞してる)
    • chai
      • ①870 ☆
      • ②昨日

現状、chaiが良さそう。(shouldはそもそもの問題もある※後述)

install

// 黒い画面から
npm install -g mocha

// htmlから使う
bower install mocha

テスト実行

$ mocha
  • 代表的なオプション
    • -R レポート出力(後述)
    • -g マッチするテストだけ実行する
    • -w js変更を監視して実行する
    • -d デバッグモード(debuggerで停止、ステップ実行できる)
    • --recursive サブディレクトリ以下も実行する
    • --reporters 使えるreporterを表示する

mochaの基本

  • インタフェースはTDD/BDD選べる
  • 幾つか見る限り、殆ど皆BDDで書いてる気がする。
  • mochaの言うTDDとBDDの違い
    • 冒頭の通りの事情で、単に表現の違い程度

BDD

// 公式ページより
describe('Array', function(){
  before(function(){
    // ...
  });

  describe('#indexOf()', function(){
    it('should return -1 when not present', function(){
      [1,2,3].indexOf(4).should.equal(-1);
    });
  });
});

TDD

// 公式ページより
suite('Array', function(){
  setup(function(){
    // ...
  });

  suite('#indexOf()', function(){
    test('should return -1 when not present', function(){
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

BDD style

  • describe
    • ネスト管理可能な階層。ディレクトリの様な物。
  • it
    • 検証内容を書く。
  • before/beforeEach
    • 前提条件
  • after/afterEach
    • 後処理

beforeとbeforeEach

  • before
    • その階層で一度しか実行しない
  • beforeEach
    • その下の階層でit毎に毎回実行される
'use strict';
(function () {
  describe('before test', function () {
    before(function(){
      console.log('before');
    });
    beforeEach(function(){
      console.log('beforeEach');
    });
    it('should show console.log', function () {
      console.log('first');
    });
    it('should show console.log', function () {
      console.log('second');
    });
  });
})();

// 以下の順番でconsole.log出力される
// before => beforeEach => first => beforeEach => second

非同期コードの書き方

  • itのfunctionの第1引数に用意されている関数を掴んでおく
    • it(‘should show’, function(done){
  • 非同期のテストが終わったタイミングで上記の関数を実行する
    • done()
// 例
describe('User', function(){
  describe('#save', function(){
    it('should save', function(done){
      var user = new User(‘taro’);
      user.save({}, {success: function(user) {
        expect(user.isNew()).to.be.not.ok
        done();
      });
    })
  })
})
  • beforeEachにも書く事が出来る
  beforeEach(function(done){
    var user = new User(‘yamada’);
    user.save({success: done});
  })

Chai

アサーション用のライブラリ should形式/expect形式/assert形式から選べる

// 公式ページより

// Should形式
chai.should();
foo.should.be.a('string');

// Expect形式
var expect = chai.expect;
expect(foo).to.be.a('string');

// Assert形式
var assert = chai.assert;
assert.typeOf(foo, 'string');

どれがいい?

should形式

  • メリット
    • 英語文法として自然なので設計を考え易い
  • デメリット
    • IEに大きな罠がある
      • yourObject.should と書くので、元のObjectにshouldプロパティを追加=>これがIEで駄目らしい(ie9でも駄目)
      • 今はどうか知らないけど、should.jsも同じらしい
    • 場合によっては、shouldが使えないcaseがある(公式Pageに書いてある)

expect形式

  • assertよりはexpectが良いかも
    • TDD=設計なので、語の選択として、assert(検証)よりexpect(期待)の方が仕様を書く事に意識がいく(といっても、俺ら日本人なのでそこまで感じないが)
    • そもそも、mochaでassert書いてる人あんまり見ない。

という事で、expectオススメ

install

npm install chai

基本的な文法

イメージ

expect(対象オブジェクト).つなぎ目.演算子.期待値

つなぎ目(language chain)

可読性を上げるためのつなぎ目となるgetter。特にテストには関係ない。

  • to
  • be
  • been
  • is
  • that
  • and
  • have
  • with
  • at
  • of
  • same

英語文法的に、expect to 〜に併せて、

expect(object).to.be.値という形が多い。

期待値

以降、見ての通り。補足必要な所だけコメント付けた。

// .ok
expect('everthing').to.be.ok; // trueらしければtrue

// .true
expect(true).be.true; // trueの時のみtrue

// .empty
expect([]).to.be.empty;

// .a(type)
expect(‘test’).to.be.a(‘string’);

// .string(string)
expect('foobar').to.have.string('bar');

// .closeTo(expected, delta)
expect(1.1).to.be.closeTo(1, 0.3); // 0.7から1.3までの値であれば、true!

演算子

// .not
expect(foo).to.not.equal('bar');

// .equal(value) 厳密等価演算子(===)で確認
expect('hello').to.equal('hello');

// .eql(value) deeply equal。object等も再起的に値チェックしてくれる
expect({ foo: 'bar' }).to.eql({ foo: 'bar' });

// .match(regexp)
expect('foobar').to.match(/^foo/);

// .exist (nullかundefinedではない事を検証)
expect(foo).to.not.exist;

その他

// .throw(constructor)
expect(fn).to.throw(ReferenceError);

// .respondTo(method)
Klass.prototype.bar = function(){};
expect(Klass).to.respondTo(‘bar’); //objectやclassがメソッド返すかチェック

// .itself
Klass.baz = function(){};
expect(Klass).itself.to.respondTo(‘baz’); // static functionを返すかチェック

// .satisfy(method) 関数内でtrueになるかチェック
expect(1).to.satisfy(function(num) { return num > 0; });

sinon.js

テストダブル(スタブ、モック等)を提供してくれるライブラリ。

テストダブルとは

テストダブルとは

Targetとなるソースが外部componentに依存している時に、

この外部コンポーネントに起因する何かしらの理由により、

テストが困難な時に、外部componentを置き換える仕組み。

※例:まだ、サーバーサイド出来ていないから、仮の関数に置換する等。

[ Target ] <= [ TestCode ]
     |
[ 外部component ]  ※これを置き換える

sinon.jsはspyとstubsとmocksを提供してくれる

spy

[ Target ] <= [ TestCode ]
     | <— spy 関数呼び出しを監視して記録 = 間接出力を保存
[ 外部component ] 

spyは、存在する関数をラップして、関数の実行時の以下の情報を保持

  • 引数の値
  • 戻り値
  • this
  • 例外

なお、存在する関数をspyした時、元の関数は通常通り振る舞う。

test("test should call subscribers on publish": function () {
    var callback = sinon.spy();
    PubSub.subscribe("message", callback);

    PubSub.publishSync("message");

    assertTrue(callback.called);
}

spy作成

// 基本形
var spy = sinon.spy();


// 既存の関数myFuncを置き換える
var spy = sinon.spy(myFunc);


// 既存のobject.methodを置き換える。
var spy = sinon.spy(object, “method”);
// ※object.method.restore()でオリジナルメソッドに置き換えれる

spy API

// spyが呼ばれたらtrue
spy.called

// 1度でもspyが呼ばれたらtrue
spy.calledOnce

// spyが指定の引数で一度でもspyが呼ばれたらtrue
spy.calledWith(arg1, arg2, …);

// 一度でも例外を投げたらtrue
spy.threw();

// 一度でも指定の値を返したらtrue
spy.returned(obj);

プロパティ関係

// thisを返す。spy.thisValues[0]で1件目の呼び出し時のthisが入ってる。
spy.thisValues

// 引数を返す。
spy.args

// 戻り値を返す
spy.returnValues

※spy.getCall(n)書くと、n番目に呼ばれたspyが帰って来る。ここからも、thisValueやargsを取得出来る。

stubs

次はスタブ

[ Target ] <= [ TestCode ]
    |
[ stubs ] 代わりの値を返信 = 間接入力をset
    
[ 外部component ] 

stubsは作られる前の関数の振る舞いを定義するために使う。

spyと異なり、存在する関数をラッピングしていた場合、元の関数は呼ばれない。

例えば、強制的に例外を発生させたかったり、ajaxさせたくない時等で使う

test("test should call all subscribers, even if there are exceptions" : function(){
    var message = 'an example message';
    var error = 'an example error message';

    var stub = sinon.stub().throws();
    var spy1 = sinon.spy();

    PubSub.subscribe(message, stub);
    PubSub.subscribe(message, spy1);

    PubSub.publishSync(message, undefined);

    assert(spy1.called);
    assert(stub.calledBefore(spy1));
}

stubs作成

var stub = sinon.stub();

var stub = sinon.stub(object, ‘method’); // stub関数で置き換えるobject.method.restore()で元の関数をrestore。

var stub = sinon.stub(object, “method”, func); // object.methoをspyでラップしたfunc関数で置換する

stubs API

// 与えられた引数の時だけメソッドをstubsする
stub.withArgs(arg1 [, arg2, … ]);

// 指定の戻り値を返す
stub.returnsArg(obj);

// 例外を返す
stub.throws();

mocks

最後にモック

[ Target ] <= [ TestCode ]
     |
[ mock 期待値と結果 => 検証 ※間接出力を検証する ] 

[ 外部component ] 

モックはstubsの機能に加えて、検証する機能を持っている

もし検証で使わなければ、failになる。

// 公式ページより
sinon.mock(jQuery).expects("ajax").atLeast(2).atMost(5);
jQuery.ajax.verify();

mocks作成

var mock = sinon.mock(obj);

// obj.methodをmock関数で上書き
var expectation = mock.expects(‘method’);

// 全てのmockメソッドをリストア
mock.restore();

// 全てのexpectationsを検証
mock.verify();

Expectation

// 例
sinon.mock(jQuery).expects("ajax").atLeast(2).atMost(5);
jQuery.ajax.verify();
var expectation = sinon.expectation.create([methodName]);
var expectation = sinon.mock();

// 最低呼び出し回数
expectation.atLeast(number);

// 一度でも呼ばれる事を期待
expectation.once();

// n回呼ばれる事を期待
expectation.exactly(number);

この後、

backboneでTDDする話をする予定なのだけど、この記事はここまで。

追記:この記事、未だに見て頂けているので、追記すると、

この記事書いた直後位で、jasmine2が出てきて、

非同期テストでdone書けるようになりました。

そのため、mochaの優位性が薄れてきました。

個人的に、mocha使う=chai使う=BDD使う=ランゲージチェインが気持ち悪く、

また、mocha使うと調べるときもchaiやらmochaやら色んなページ行き来しないといけないので、

最近は、jasmine2がオススメです。