確認したバージョン
node v6.9.1
mocha @3.1.2
sinon @1.17.6
describeとitの処理順番
次の様なテストを書いたとします。
ログはどの様に出力されるでしょうか?
// test.js
describe('doHogeのテスト', () => {
console.log(0);
describe('fugaな時', () => {
console.log(1);
it('fooはpiyoになる', () => {
console.log(2);
});
console.log(3);
it('barはpiyoyoになる', () => {
console.log(4);
});
console.log(5);
});
console.log(6);
});
..
....
.......
でっでー
before, after などをよく使う方は分かったかと思うのですが、
0,1,2,3,4,5,6 とは出ません。
実際に動かした結果がこちら
$ $(npm bin)/mocha test.js
0
1
3
5
6
doHogeのテスト
fugaな時
2
✓ fooはpiyoになる
4
✓ barはpiyoyoになる
2 passing (7ms)
itの外側にある0,1,3,5,6が先に出力され、it内にある2,4が後から出力されています。
なぜ?
before, afterなどの処理のためだと思われます。(mocha内を読もうとしたのですが挫折しましたorz)
mochaはdescribeを実行し、途中で呼ばれた before, beforeEach, it, afterEach, afterを拾って貯めていき、その後describe毎に、
- before
- beforeEach
- it
- afterEach
- beforeEach
- it
- afterEach
- ...(itの分だけ繰り返し)
- after
という順番で呼び出されます。
itだけ使っている忘れがちですが、こんな理由でitの外と中で処理順番が変わってくるのです。
気をつけたいこと with Sinon (本題)
ここにはまった
前置きが長くなりましたが、ここが伝えたかったところです。結構はまったのでw
私はうっかりこんなテストを書きました。
// test.js
import sinon from 'sinon';
import { expect } from 'chai';
import * as funcs from './functions'; // テスト対象
import * as utils from './utils'; // テスト対象の内部で呼ばれるやつ
describe('doHogeのテスト', () => {
describe('fugaな時', () => {
// 呼ばれる(1)
// テスト対象のfuncs.doHogeの内部で呼ばれるutils.isHogeをスタブ化
const stub = sinon.stub(utils, 'isHoge', () => true);
it('fooはpiyoになる', () => {
// 呼ばれる(3)
const actual = funcs.doHoge('foo', 'fuga');
expect(actual).to.equal('piyo');
});
it('barはpiyoyoになる', () => {
// 呼ばれる(4)
const actual = funcs.doHoge('bar', 'fuga');
expect(actual).to.equal('piyoyo');
});
// 呼ばれる(2)
// スタブ解除
stub.restore();
});
});
前述の通り、実際のテストが実行されるitのコールバック(第二引数)は一度収集され、のちの実行されます。
ですので、テストのためにスタブ化したisHogeは、テストが実行される前にrestoreされてしまっているのです。
回避の例:関数化してit内から呼ぶ
before, afetrを使っても良いと思うのですが、ここではスタブ化処理を関数に入れたものを紹介します。
describe('doHogeのテスト', () => {
describe('fugaな時', () => {
// スタブ化->テスト対象の実行→スタブ化の解除をまとめた関数
// これをitの中から呼ぶことで、it毎に確実にスタブ化+解除が実行される
const exec = (input) => {
// テスト対象のfuncs.doHogeの内部で呼ばれるutils.isHogeをスタブ化
const stub = sinon.stub(utils, 'isHoge', () => true);
const actual = funcs.doHoge(input, 'fuga');
// スタブ解除
stub.restore();
return actual;
};
it('fooはpiyoになる', () => {
const actual = exec('foo');
expect(actual).to.equal('piyo');
});
it('barはpiyoyoになる', () => {
const actual = exec('bar');
expect(actual).to.equal('piyoyo');
});
});
});
スタブ化したい内容や、テストしたい値の受け取り方次第で書き方も変わるので、実行順番に気をつけて、都度最適化する必要がありますが、1例として参考にしていただければ幸いです。
まとめ
上から書いた順に実行されないから気をつけて!!!
## おまけ 説明しやすいようにサンプルコードを書きましたが、本当に書くときはループで書くとよいですよ。
describe('doHogeのテスト', () => {
// テスト条件をオブジェクトにまとめる
const dataProvider = {
'fugaな時、fooはpiyoになる' : {
data : {
isHoge : true,
arg1 : 'foo',
arg2 : 'fuga',
},
expected : 'piyo',
},
'fugaな時、barはpiyoyoになる', () => {
data : {
isHoge : true,
arg1 : 'bar',
arg2 : 'fuga',
},
expected : 'piyo',
},
};
// 実行部
const exec = (data) => {
// テスト対象のfuncs.doHogeの内部で呼ばれるutils.isHogeをスタブ化
const stub = sinon.stub(utils, 'isHoge', () => data.isHoge);
const actual = funcs.doHoge(data.arg1, data.arg2);
// スタブ解除
stub.restore();
return actual;
};
// ガンガン回してテスト
for (let [describe, testCase] of Object.entries(dataProvider)) {
it(describe, () => {
const actual = exec(testCase.data);
expect(actual).to.equal(testCase.expected);
});
};
});