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
- テスト駆動開発
- by ケントベック
特徴
- 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
- 去年くらいまで: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できる?
- 学習コストは?
- QUnit < jasmine < mocha の順に学習量は多くなる。
TestFWを整理
あまりこだわりなければ、とりえあず、今は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');
も書ける
- 上記、sholdやexpectいずれも使える。他に、xUnit(TDD)みたいな
- should.js:rspec(BDD)みたい
参考データ(2013/11/30現在の①github star数と②github更新状況)
- should.js
- ①1241 ☆
- ②昨日
- expect.js
- ①524 ☆
- ②半年前(更新停滞してる)
- chai
- ①870 ☆
- ②昨日
- should.js
現状、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形式
- メリット
- 英語文法として自然なので設計を考え易い
- デメリット
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がオススメです。