web-dev-qa-db-ja.com

ES6モジュールのインポートを偽造するにはどうすればいいですか?

次のES6モジュールがあります。

network.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

私はgetDataFromServerのモックインスタンスでWidgetをテストする方法を探しています。カルマのように、ES6モジュールの代わりに別の<script>を使用した場合は、次のようにテストを書くことができます。

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

ただし、ES6モジュールをブラウザの外部で個別にテストする場合(Mocha + babelの場合のように)、次のように書きます。

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

さて、しかし今はgetDataFromServerwindowで利用できません(そして、windowはまったくありません)、そして私はwidget.js自身のスコープに直接ものを注入する方法を知りません。

それで、私はここからどこに行きますか?

  1. widget.jsの範囲にアクセスする方法、または少なくともそのインポートを自分のコードに置き換える方法はありますか?
  2. そうでなければ、どうやってWidgetをテスト可能にすることができますか?

私が考えたもの:

a。手動の依存性注入.

widget.jsからすべてのインポートを削除し、呼び出し元がdepsを提供することを期待します。

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

私はこのようにWidgetの公開インターフェースをめちゃくちゃにして実装の詳細を公開することに非常に不快です。立ち入り禁止。


b。それらをあざけることができるようにインポートを公開します。

何かのようなもの:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

その後:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

これはそれほど侵略的ではありませんが、各モジュールのためにたくさんの定型文を書く必要があり、それでも私はいつもdeps.getDataFromServerの代わりにgetDataFromServerを使う危険があります。私はそれについて不安です、しかしそれはこれまでのところ私の最高のアイデアです。

126
Kos

私のテストではimport * as objスタイルを使い始めました。これは、モジュールからのすべてのエクスポートを、後でモックできるオブジェクトのプロパティとしてインポートするものです。私はこれをrewireやproxyquire、あるいは他の似たようなテクニックを使うよりもずっときれいだと思います。たとえば、Reduxのアクションをモックする必要があるときに、これを最も頻繁に行いました。これが上記のあなたの例のために使うかもしれないものです:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

あなたの関数がたまたまデフォルトのエクスポートであるなら、import * as network from './network'{default: getDataFromServer}を生成するでしょう、そしてあなたはnetwork.defaultをモックすることができます。

116
carpeliam

@carpeliamは正しいですが、モジュール内の関数を調べてその関数を呼び出すそのモジュール内で別の関数を使用したい場合は、exports名前空間の一部としてその関数を呼び出す必要があることに注意してください。

間違った例:

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});

正しい例:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});
28
vdloo

@ vdlooの答えで私は正しい方向に進んだが、同じファイル内でcommonjsの "exports"キーワードとES6モジュールの "export"キーワードの両方を一緒に使用してもうまくいかなかった(webpack v2以降の苦情)。代わりに、デフォルトの(名前付き変数)エクスポートを使用して、個々の名前付きモジュールのエクスポートをすべてラップしてから、デフォルトのエクスポートをテストファイルにインポートします。私はmocha/sinonで以下のエクスポート設定を使用しており、スタブは再配線などを必要とせずにうまく動作します。

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});
7
QuarkleMotion

私は、明示的な依存性注入について知るために元のクラスを必要とせずに、TypeScriptクラスインポートのランタイムモックの問題を解決しようとするライブラリを実装しました。

ライブラリはimport * as構文を使用してから、エクスポートされた元のオブジェクトをスタブクラスに置き換えます。対応するテストを更新せずにメソッド名が更新された場合、あなたのテストはコンパイル時に壊れるので、型安全性を保持します。

このライブラリはここにあります: ts-mock-imports

4
EmandM

私はこの構文がうまくいっているのを見つけました:

私のモジュール:

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

私のモジュールのテストコード:

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

doc を参照してください。

3
nerfologist