Sinon を使用した ES6 モジュールのテスト

✍🏼 作成日 2019年11月10日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください
🖥  説明:この1週間、前任者のテストコードを修正していて、Sinonを使う中でいくつかの問題に遭遇したので、記録しておきます。

はじめに

以下ではSinonのspyインターフェースについてのみ説明しますが、stubなどのインターフェースにも同様に適用できます

テストではMocha Sinon Chaiライブラリ/フレームワークを使用していますが、ここでは紹介しません

单独でエクスポートされたモジュールをspyする

以下のようなfoo.jsモジュールがあり、foo関数をエクスポートしています

1
2
3
export function foo {
return 'foo';
}

以下のようなbar.jsモジュールがあり、このfoo関数をインポートしています

1
2
3
4
import { foo } from './foo';
export function bar {
return 'bar' + foo();
}

以下のようなテストケースbar.test.jsがあります

1
2
3
4
5
6
7
8
9
import { bar } from './bar';
describe('ES6 导出模块测试-单独导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy(bar);
const result = bar();

expect(spy.called).to.equal.true; // 失败
});
});

ここで失敗する理由は、bar.test.jsでインポートしたbarが関数を含む変数であり、Sinonはこの変数barをspyしているだけで、barに対応する関数をspyしていないからです。以下のテストケースも同じ状況です:

1
2
3
4
5
6
7
8
9
import { bar as baz } from './bar';
describe('ES6 导出模块测试-单独导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy(baz);
const result = baz();

expect(spy.called).to.equal.true; // 失败
});
});

この状況は次のセクションの内容で解決できます:

全部でエクスポートされたモジュールをspyする

1
2
3
4
5
6
7
8
9
import * as allBar from './bar';
describe('ES6 导出模块测试-全部导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy(allBar, 'bar');
const result = allBar.bar();

expect(spy.called).to.equal.true; // 成功
});
});

では、エクスポートされていない関数をどのようにテストすればよいでしょうか?例えば最初のテストケースで、fooが呼び出されたかどうかをテストするにはどうすればよいでしょうか?

ここには2つの方法があります。1つ目は純粋なES6の方法で、インポートしたfoo関数をbar内で再度エクスポートするしかありません(これでは测试未导出函数の前提条件に違反してしまいます)。

2つ目の方法はbabelプラグインを使用する方法です。この方法の本質はES6をES5に変換してからテストすることです。プラグインの名前はbabel-plugin-rewireで、presetタイプのプラグインです。

rewireとは文字通り「再配線」という意味で、このプラグインは可以将某个模块中导入的但是并未导出却在该模块中调用的函数进行重新导出以方便测试少し説明が難しいので、完全版の例を見てみましょう:

foo.jsがあります:

1
2
3
export function foo() {
return 'foo';
}

foo.jsをインポートしているがfooをエクスポートしていないbar.jsがあります:

1
2
3
4
5
import { foo } from './foo';
export default function bar() {
// 注意此处 默认导出 export default 很重要, 原因下面说
return 'bar' + foo();
}

テストファイルbar.test.js:

1
2
3
4
5
6
7
8
9
10
import bar from './bar'; // 这里叫 bar , 其实叫任何名字都可以, 因为是默认导出
describe('ES6 导出模块测试-默认导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy();
bar.__Rewire__('foo', spy); // 注意这里的用法和 __Rewire__ 方法
const result = bar();

expect(spy.called).to.equal.true; // 成功
});
});

コメントにあるように、デフォルトエクスポートは重要です。なぜなら、デフォルトエクスポート上の__Rewire__プロパティを通じてのみ再rewireが可能であり、以下のようにテストすることはできないからです:

1
2
3
4
5
6
7
8
9
10
import { bar } from './bar'; // 假设 bar.js 中 bar 函数不是默认导出
describe('ES6 导出模块测试-默认导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy();
bar.__Rewire__('foo', spy); // 这里会报 __Rewire__ 不是函数
const result = bar();

expect(spy.called).to.equal.true; // 失败
});
});

前述の全部导出のような場合でも実現できません:

1
2
3
4
5
6
7
8
9
10
11
12
import * as allBar from './bar'; // 假设 bar.js 中 bar 函数不是默认导出
describe('ES6 导出模块测试-默认导出', () => {
it('应该能够 spy bar', () => {
const spy = sinon.spy();
allBar.__Rewire__('foo', spy); // 这里不会报错, 但是测试不通过, 因为 allBar 上并没有 foo 方法(因为是在 bar 函数中调用的)
// 或者下面也不行会报 __Rewire__ 不是函数错误, 因为 __Rewire__ 并不重新 rewire 全部导出对象上的属性
// allBar.bar.__Rewire__('foo', spy);
const result = bar();

expect(spy.called).to.equal.true; // 失败
});
});

もし全部导出を使用して非公開関数をテストしたい場合、テスト対象ファイルはその関数が根作用域という条件を満たす必要があります。例えば:

1
2
3
4
5
import { foo } from './foo';
export foo;
export function bar () {
return 'bar' + foo();
}

この場合、テストファイルは次のように記述できます:

1
2
3
4
import * as allBar from './bar';
// 省略无关部分
const spy = sinon.spy();
allBar.__Rewire__('foo', spy); // 这么做就对了, foo 函数位于 allBar 的根作用域中

終わりに

注意点として、これは関数テストだけでなく、デフォルトエクスポートが関数/クラスであるReactコンポーネントにも適用されます。なぜなら、それらのエクスポートの本質は同じく関数またはオブジェクトだからです。Reactコンポーネントをテストする際にはenzymeライブラリを使用するかもしれません。

- EOF -
この記事の初出: Sinon を使用した ES6 モジュールのテスト - Xheldon Blog