LoginSignup
9
12

More than 5 years have passed since last update.

mochaでテストを書くときに気をつけたいこと with Sinon.JS

Posted at

確認したバージョン

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);

});

 
 
..
 
 
 
....
 
 
 
.......
 
 
 :no_good:でっでー

 
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);
        });
    };
});
9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12