How can I mock the imports of an ES6 module?

Kos picture Kos · Feb 6, 2016 · Viewed 67.5k times · Source

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.

Answer

carpeliam picture carpeliam · Jul 16, 2016

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.