mocha で行うユニットテスト内でスパイ・モック化するなら「sinon」

以前、mocha という npm パッケージを使った単体テスト環境を構築したが、この mocha はテストランナーとしての側面が強く、特定のメソッドをモック化したりする機能は有していない。

Jasmine のように、spyOn().and.callFake()toHaveBeenCalled() といったことをやりたく、似たようなライブラリがないか調べたところ、sinon というパッケージを使う例が多く見つかった。

目次

インストール

インストールはいつもどおり。テストランナーである mocha と、結果値検証に使う expect も入れてみる。

$ npm install --save-dev mocha expect sinon

指定の関数が呼ばれたか確認する

まずは、ある関数が呼ばれたかを確認するための、sinon.spy().called を使ってみる。Jasmine の toHaveBeenCalled() 相当だ。

テスト対象のコードは以下のとおり。

/** テスト対象のクラス */
class HogeClass {
  /** テスト対象の関数 */
  execHoge(value, callback) {
    console.log(value);
    callback();
  }
}

コレに対するテストコードは以下のとおり。

const expect = require('expect');
const sinon = require('sinon');

const HogeClass = require('../src/hoge');

describe('execHoge() メソッドのテスト', () => {
  it('第2引数のコールバック関数が呼ばれること', () => {
    // 第2引数に指定するコールバック関数をスパイで作る
    const stubCallback = sinon.spy();
    // テスト対象関数を実行する
    const hogeClass = new HogeClass();
    hogeClass.execHoge('Test', stubCallback);
    // スパイ関数が実行されたかどうかを確認する
    expect(stubCallback.called).toBe(true);
  });
});

こんな感じ。

ある関数をモック化し、テスト用の戻り値を返す

Jasmine における and.callFake()and.returnValue() 相当。

class HogeClass {
  // FugaClass を DI (依存性注入) するような実装
  constructor(fugaClass) {
    this.fugaClass = fugaClass;
  }
  
  // FugaClass の exec() を実行し、その戻り値を2倍する関数
  execFuga(value) {
    const result = this.fugaClass.exec(value);
    return result * 2;
  }
}

ココで、DI する FugaClass がどのように実装されているかは、単体テストにおいては関係ない。

const expect = require('expect');
const sinon = require('sinon');

const HogeClass = require('../src/hoge');

describe('execFuga() メソッドのテスト', () => {
  // FugaClass.exec() メソッドのモックを控える変数
  let stubFugaExec = null;
  
  it('計算結果を2倍して返すこと', () => {
    // とりあえずインスタンスを生成する
    const dummyFugaClass = {
      exec: () => { /* ダミー */ }
    };
    const hogeClass = new HogeClass(dummyFugaClass);
    
    // FugaClass.exec() メソッドの動作をモック化する
    stubFugaExec = sinon.stub(hogeClass.fugaClass, 'exec').callsFake((value) => {
      // 引数が 10 であることを確認する
      expect(value).toBe(10);
      // 戻り値を固定で渡す
      return 1;
    });
    // もしくは sinon.stub().returns(1);
    
    // テスト対象関数を実行する (引数はデタラメでもいい)
    const result = hogeClass.execFuga(10);
    
    // 結果が「1」を2倍にした値であること
    expect(result).toBe(2);
  });
  
  afterEach(() => {
    // スタブで変更した関数を元に戻す
    if(stubFugaExec && stubFugaExec.restore) {
      stubFugaExec.restore();
    }
  });
});

こんな感じで一応テストはできる。

テスト対象クラスがテキトーすぎてイマイチ分かりづらいかもだけど、Jasmine における spyOn() と同等の機能が sinon.stub() である。.and.callFake().and.returnValue() といった繋ぎ方ではなく、.callsFake().returns() という風に書くので、対応するメソッド名を確認されたし。

そして Jasmine の spyOn() と大きく違うのは、モック化した関数が1つのテストケース (it()) 内でリセットされず、残り続けてしまうという挙動だ。そのために、sinon.stub() の戻り値である SinonStub クラスのインスタンスを変数に控えておき、1つのテストケースが終了した段階で .restore() メソッドを呼んで元の関数に戻してあげている。コレを怠って、再度同じ関数に sinon.stub() を適用しようとすると、例外が発生するので注意。

以上

sinon における Spy と Stub の違いがちょっと怪しい…。一応こんな感じでテストはできたものの、もっと効率的な書き方もありそうだ。