ES6モジュールのインポートをモックする方法は?


141

次の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<script>KarmaのようにES6モジュールの代わりに個別のを使用した場合、次のようなテストを作成できます。

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

わかりましたが、現在getDataFromServerは使用できませんwindow(まあ、まったくありませんwindow)。また、をwidget.js自分のスコープに直接挿入する方法がわかりません。

ここからどこへ行くのですか?

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

私が考えたもの:

a。依存関係の手動注入。

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

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

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


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

これは侵襲性は低くなりますが、モジュールごとに多くのボイラープレートを作成する必要がありgetDataFromServerdeps.getDataFromServer常に使用するのではなく、使用するリスクがまだあります。私はそれについて不安ですが、それは今のところ私の最高の考えです。


この種のインポートに対するネイティブのモックサポートがない場合、ES6スタイルのインポートをカスタムのモック可能なインポートシステムに変換する独自のトランスフォーマーを作成することを考えます。これにより、考えられる失敗の別のレイヤーが追加され、テストするコードが変更されます。
t.niese

現在、テストスイートを設定することはできませんが、jasmincreateSpygithub.com/jasmine/jasmine/blob/…)関数を 'network.js'モジュールからインポートされたgetDataFromServerへの参照で使用しようとしています。そのため、ウィジェットのテストファイルでは、getDataFromServerをインポートしてから、let spy = createSpy('getDataFromServer', getDataFromServer)
16

2番目の推測は、関数ではなく、「network.js」モジュールからオブジェクトを返すことです。このようにして、モジュールspyOnからインポートされたそのオブジェクトに対して実行できnetwork.jsます。常に同じオブジェクトへの参照です。
Microfed、2016

実際には、オブジェクトが、私が見ることができるものから、すでにです:babeljs.io/repl/...
Microfed

2
依存性注入がWidgetのパブリックインターフェースをどのように混乱させるのか本当に理解できませんか?なしWidgetでめちゃくちゃです。依存関係を明示的にしないのはなぜですか? deps
thebearingedge

回答:


129

私は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をモックできます。


3
あなたは使いますかimport * as obj唯一のテストで、あるいはまた、あなたの通常のコードでは?
Chau Thai

36
@carpeliamこれは、インポートが読み取り専用であるES6モジュール仕様では機能しません。
アシシュ

7
ジャスミンは不平を言っています [method_name] is not declared writable or has no setteres6のインポートは一定であるため、をいます。回避策はありますか?
lpan 2017年

2
@Franciscはimportrequireどこにでも移動できるのとは異なり)巻き上げられるため、技術的に複数回インポートすることはできません。あなたのスパイが他の場所で呼び出されているように聞こえますか?テストが混乱しないようにするため(テスト汚染と呼ばれます)、スパイをafterEachでリセットできます(例:sinon.sandbox)。ジャスミンはこれを自動的に行うと思います。
カーペリアム2017年

10
@ agent47問題は、ES6仕様ではこの答えが機能しないことを明確に示していますが、あなたが述べたとおりに、importJS を記述するほとんどの人は実際にはES6モジュールを使用していません。webpackやbabelのようなものがビルド時に組み込まれ、それをコードの遠方の部分を呼び出すための独自の内部メカニズム(など__webpack_require__)に変換するか、ES6より前の事実上の標準の1つであるCommonJS、AMDまたはUMDに変換します。そして多くの場合、その変換は仕様に厳密に準拠していません。したがって、多くの開発者にとって、この答えはうまくいきます。今のところ。
daemonexmachina

31

@carpeliamは正しいですが、モジュール内の関数をスパイし、そのモジュールでその関数を呼び出す別の関数を使用する場合は、エクスポート名前空間の一部としてその関数を呼び出す必要があります。そうしないと、スパイは使用されません。

間違った例:

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

4
私はこの答えを20回以上投票できればいいのに!ありがとうございました!
sfletche

これが事実である理由を誰かが説明できますか?exports.myfunc2()は直接参照せずにmyfunc2()のコピーですか?
Colin Whitmarsh 2017

2
@ColinWhitmarsh exports.myfunc2は、spy関数への参照に置き換えられるmyfunc2までの直接参照spyOnです。spyOnの値を変更exports.myfunc2してスパイオブジェクトに置き換えますmyfunc2が、モジュールのスコープでspyOnは変更されません(アクセスできないため)
madprog

*オブジェクトをフリーズしてインポートしないでください。オブジェクトの属性は変更できませんか?
agent47

1
export function一緒に使用することのこの推奨事項exports.myfunc2は、技術的にcommonjsとES6モジュールの構文を混合するものであり、これはオールオアナッシングES6モジュール構文の使用を必要とする新しいバージョンのWebpack(2+)では許可されていません。ES6の厳密な環境で機能するこの問題に基づいて、以下の回答を追加しました。
QuarkleMotion

6

Typescriptクラスのインポートの実行時のモックの問題を解決するために、元のクラスが明示的な依存関係の注入について知る必要なくライブラリを実装しました。

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

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


1
このモジュールには、より多くのgithubスターが必要です
SD

6

@vdlooの答えは正しい方向に向かいましたが、commonjsの「exports」とES6モジュールの「export」の両方のキーワードを同じファイルで一緒に使用しても機能しません(webpack v2以降の文句)。代わりに、デフォルトの(名前付き変数)エクスポートを使用して、個々の名前付きモジュールエクスポートをすべてラップし、デフォルトのエクスポートをテストファイルにインポートしています。私はモカ/シノンで次のエクスポート設定を使用しており、スタブは再配線などを必要とせずに正常に機能します。

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

役立つ回答、ありがとうございます。let MyModuleはデフォルトのエクスポートを使用する必要がないことを述べたかっただけです(それは生のオブジェクトである可能性があります)。また、このメソッドはmyfunc1()を呼び出す必要がなく、myfunc2()直接スパイするように機能します。
マークエディントン2018

@QuarkleMotion:誤ってメインアカウントとは異なるアカウントでこれを編集したようです。そのため、編集は手動で承認する必要がありました。あなたからの編集ではないようです。 これは単なる事故だと思いますが、意図的なものである場合は、靴下人形のアカウントに関する公式ポリシーを読んで、誤ってルールに違反しないでください
目立つコンパイラ

1
@ConspicuousCompilerへの感謝の言葉-これは間違いでした。私は自分の仕事のメールリンクされたSOアカウントでこの回答を変更するつもりはありませんでした。
QuarkleMotion、

これは別の質問への回答のようです!widget.jsとnetwork.jsはどこにありますか?この回答には推移的な依存関係がないようで、元の質問が困難になりました。
Bennett McElwee

3

この構文が機能していることがわかりました:

私のモジュール:

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

ドキュメントを参照してください。


+1といくつかの追加手順:ノードモジュール、つまり、package.jsonにあるものでのみ機能するようです。さらに重要なのは、Jestドキュメントで言及されていないことです。渡される文字列はjest.mock()、定数の名前ではなく、import / packge.jsonで使用される名前と一致する必要があります。ドキュメントでは、彼らは両方とも同じですが、のようなコードを使用してimport jwt from 'jsonwebtoken'、あなたのようにモックセットアップを必要とするjest.mock('jsonwebtoken')
kaskelotti

0

私自身は試していませんが、あざけることがうまくいくと思います。実際のモジュールを、提供したモックで置き換えることができます。以下は、それがどのように機能するかを理解するための例です。

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

mockeryもはや維持されていないようで、Node.jsでのみ機能すると思いますが、それでもモックするのが難しいモジュールをモックするための優れたソリューションです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.