[ES2015]importされたモジュールのインスタンス化をsinonを用いて乗っ取ることができない時はinject-loaderで解決
[ES2015]importされたモジュールのインスタンス化をsinonを用いて乗っ取ることができない時はinject-loaderで解決:
以下の状況下で困っている方に向けた記事です
※karmaでmocha, sinonを用いていることを前提としています
https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L63
chromeの場合オブジェクトリテラルによって作られたオブジェクトにはconstructorプロパティが存在することになります。
https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L68
ですのでchromeだとここの分岐に入ってくれません。
https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L81
phantomJSではこのエラーを吐かなかったのでおそらく
そもそも
importをのっとれば良い
依存関係にのっとったオブジェクトを注入してしまえば良いのです。
injejct-loaderを使います
このnpmモジュールはimport文で読み込むもとのモジュールを置き換えることが可能です。
必ずしも
sinonのissueでは'proxyquire'がおすすめされています。
どうしても
https://github.com/sinonjs/sinon/issues/831#issuecomment-209648966
はじめに
以下の状況下で困っている方に向けた記事です- es2015でbabelでトランスパイルすることを前提にテストコードを書いている
- テスト対象のモジュール[Foo]が別のモジュール[Bar]をimportしていて、[Bar]のインスタンス化をテストしたい
困っていたこと
※karmaでmocha, sinonを用いていることを前提としています- exportされたクラスがimportされた場所毎に異なるコンストラクタ関数オブジェクトになっている
- そのせいでテスト対象のモジュールがimportしているコンストラクタ関数はspyか不可能
- 異なるコンテキストのコンストラクタ関数でもprototypeオブジェクトは共通らしいので、prototypeオブジェクトをmock化してconstructorメソッドをのっとればいいじゃん
- エンジンによってそれで良いパターンとだめなパターンがある
- 例) phantomJSではその方法でいけるが、chromeでアクセスするとだめ
- そもそもes2015のconstructorって・・・
- エンジンによってそれで良いパターンとだめなパターンがある
実際にコードでお見せします
module[Foo(../src/foo)]
import Bar from "../src/bar"; export default class Foo { constructor() { this.hoge = 'hoge'; } fuga() { const bar = new Bar(); bar.piyo(); } }
module[Bar(../src/bar)]
export default class Bar { constructor() { this.fuga = 'fuga'; } piyo() { alert('piyo'); } }
spec[Foo]
import assert from "power-assert"; import Foo form "../src/foo"; import Bar from "../src/bar"; describe("Fooクラスのテスト", () => { it("fugaメソッドの呼び出しでBarクラスがインスタンス化される", () => { // クラスBar(ES5までで言えばコンストラクタ関数Bar)は、 // FooクラスでimportされているBarとは異なる。だからspyはできない // spyできたら楽 // const spy = sinon.spy(Bar); // const foo = new Foo(); // foo.fuga(); // const isBarCalledWithNew = spy.calledWithNew(); // assert(isBarCalledWithNew); // => expected true, but false returns // しかしprototypeオブジェクトは共通なようなのでmockにする const mock = sinon.mock(Bar.prototype); mock.expects('constructor').once(); // ここでコケる // ちなみにconstructor以外のメソッドは大丈夫(実行環境でObject.prototypeが持っているメソッド以外は大丈夫) const foo = new Foo(); foo.fuga(); const isBarCalledWithNew = mock.verify(); assert(isBarCalledWithNew); }); });
mock化して疑似constructorを作ろうとするとこける原因
実行環境によってはObject.prototypeの挙動が異なるようです
https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L63mock.js#L63
this.expectations = {};
https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L68
mock.js#L68
if (!this.expectations[method]) {
エラーの場所
this.expectations[method]
がundefined
になり、下記行でエラーになります。https://github.com/sinonjs/sinon/blob/master/lib/sinon/mock.js#L81
mock.js#L81
push(this.expectations[method], expectation);
Object.prototype
の仕様が異なるようです。そもそも
constructor
プロパティってprototype
オブジェクトがコンストラクタ関数を参照するための属性なのでimportごとに文脈が異なるコンストラクタ関数を乗っ取れない以上無駄なアプローチだと思っています。(phantomJSではいけましたが)
解決策
importをのっとれば良い依存関係にのっとったオブジェクトを注入してしまえば良いのです。
どうやって
injejct-loaderを使いますこのnpmモジュールはimport文で読み込むもとのモジュールを置き換えることが可能です。
必ずしも
sinon
と連携しなくても、非同期通信用モジュールを書き換えたりする用途で仕様可能です。
実際のコード
spec[Foo]
import assert from "power-assert"; import injector from "inject-loader!../src/foo"; import Bar from "../src/bar"; const barSpy = sinon.spy(Bar); const Foo = injector({ // Foo内で使っているBarモジュールをモック化 "../src/bar": barSpy }).default; describe("Fooクラスのテスト", () => { it("fugaメソッドの呼び出しでBarクラスがインスタンス化される", () => { // spyできた! const foo = new Foo(); foo.fuga(); const isBarCalledWithNew = barSpy.calledWithNew(); assert(isBarCalledWithNew); // => true }); });
おわりに
sinonのissueでは'proxyquire'がおすすめされています。どうしても
import
構文を使いたいってわけでもなく、require
でモジュール読み込んでる場合はこれで良いと思います。https://github.com/sinonjs/sinon/issues/831#issuecomment-209648966
コメント
コメントを投稿