Mocking ES6 module import with and without Dependency Injection

Mocking ES6 module import with and without Dependency Injection

ES6 module system is a great addition to JavaScript language, but sometimes it’s at odds with writing tests, because you need to mock imported function.

Usual solution is to use Dependency Injection akin to what is used in Angular.JS. You need to wrap the whole module into some function and explicitly pass required dependencies. While such approach works fine, it mangles code and adds an additional layer of indirection.

There are two alternative approaches. One is by using mocking library, which is simpler. Another one is more complex and uses Dependency Injection, but in a way, which does not cause big changes to initial code. Using mocking library works fine at the moment, because ES6 modules are transpiled into ES5 objects. But it could not work with real ES6 modules and then Dependency Injection way can be handy.

Mocking with Sinon.JS

Let’s consider feature.js module, which looks like this:

// feature.js module
import { fetchData } from './backend';

export function doSomething() {
  // some code which calls fetchData
}

feature.js imports fetchData function from backend.js. The goal is to mock fetchData call in feature.js when writing functional tests. In our example, we will use Sinon.JS, but Jasmine can be used as well.

// feature.test.js module
import { expect } from 'chai';
import sinon from 'sinon';
import { doSomething } from './feature';
import * as Backend from './backend';

describe.only('doSomething', () => {
  it('does something meaningful', () => {
    sinon.stub(Backend, 'fetchData');
    // use doSomething with mocked fetchData
  });
});

So, the main trick here is to import the whole module:

import * as Backend from './backend';

and stub/mock required call:

sinon.stub(Backend, 'fetchData');

Mocking with Dependency Injection

To manually mock the function, the simplest way would be to reassign fetchData to some mock-function, but imported bindings are read-only. So, we need to make a little trick:

// feature.js module
import { fetchData as originalFetchData } from './backend';

let fetchData = originalFetchData;

export function mock(mockedFetchData) {
  fetchData = mockedFetchData || originalFetchData;
}

export function doSomething() {
  // some code which calls fetchData
}

Now, instead of directly using fetchData from backend.js we imported it as originalFetchData. Our local variable fetchData can be reassigned to anything we want. By default it is bound to originalFetchData. To mock it we need to call mock and pass a mocked version. In case we need to unmock, just call mock with no params.

An example of usage in test:

// feature.test.js module
import { expect } from 'chai';
import { mock, doSomething } from './feature';

const dummyFetchData = () => {};

describe('doSomething', () => {
  beforeEach(() => { mock(dummyFetchData); });
  afterEach(() => { mock(); });

  it('does something meaningful', () => {
    // use doSomething with mocked fetchData
  });
});

Mocking Module

There is a slight variation of this approach, if we need to mock a set of calls from one module:

// feature.js module
import * as Backend from './backend';

let { fetchData, saveData, deleteData } = Backend;

export function mock(mockedBackend) {
  ({ fetchData, saveData, deleteData } = mockedBackend || Backend);
}

// some code which calls functions from Backend

Here we import the whole module and cherry-pick required calls. The rest is the same as before, but we need to pass an object with mocked calls into mock.