How can I mock the imports of an ES6 module? Ask Question

How can I mock the imports of an ES6 module? Ask Question

I have the following ES6 modules:

File network.js

export function getDataFromServer() {
  return ...
}

File widget.js

import { getDataFromServer } from 'network.js';

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

  render() {
    ...
  }
}

I'm looking for a way to test Widget with a mock instance of getDataFromServer. If I used separate <script>s instead of ES6 modules, like in Karma, I could write my test like:

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

However, if I'm testing ES6 modules individually outside of a browser (like with Mocha + Babel), I would write something like:

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

Okay, but now getDataFromServer is not available in window (well, there's no window at all), and I don't know a way to inject stuff directly into widget.js's own scope.

So where do I go from here?

  1. Is there a way to access the scope of widget.js, or at least replace its imports with my own code?
  2. If not, how can I make Widget testable?

Stuff I considered:

a. Manual dependency injection.

Remove all imports from widget.js and expect the caller to provide the deps.

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

I'm very uncomfortable with messing up Widget's public interface like this and exposing implementation details. No go.


b. Expose the imports to allow mocking them.

Something like:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

then:

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

This is less invasive, but it requires me to write a lot of boilerplate for each module, and there's still a risk of me using getDataFromServer instead of deps.getDataFromServer all the time. I'm uneasy about it, but that's my best idea so far.

ベストアンサー1

I've started employing the import * as obj style within my tests, which imports all exports from a module as properties of an object which can then be mocked. I find this to be a lot cleaner than using something like rewire or proxyquire or any similar technique. I've done this most often when needing to mock Redux actions, for example. Here's what I might use for your example above:

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

If your function happens to be a default export, then import * as network from './network' would produce {default: getDataFromServer} and you can mock network.default.

Note: ES仕様ではモジュールを読み取り専用として定義しており、多くのESトランスパイラがこれを尊重し始めているため、このスパイスタイルが破られる可能性があります。これは、トランスパイラとテストフレームワークに大きく依存します。たとえば、Jestはこれを機能させるために何らかの魔法をかけていると思いますが、ジャスミンは、少なくとも現時点では. 人によって違います。

おすすめ記事