3.1 Mocking CommonJS and ESM imports

JavaScript module import/require testing do’s and don’ts with Jest.

The previous sections showed how to create mock objects and stub out global objects.

This section goes through how to achieve different types of module mocking scenarios with Jest.

From simple Import interception, to how to approach stubbing out an internal function call or Mocking only part of a module (by spying…).

These are methods that work in more specific cases than what the Jest official documentation shows.

This isn’t strictly a Jest testing guide, the same principles can be applied to any application/tests that need to mock CommonJS or ES Modules.

Types of imports

Modern JavaScript has 2 types of imports:

  • CommonJS: Node.js’ built-in import system which uses calls to a global require('module-y') function, packages on npm expose a CommonJS compatible entry file.
  • ES Modules (ESM): modules as defined by the ECMAScript standard. It uses import x from 'module-y' syntax.

There are also (legacy) module loaders like RequireJS and AMD but CommonJS and ESM are the current and future most widespread module definition formats for JavaScript.

ES Modules have 2 types of exports: named exports and default exports.

A named export looks likes this: export function myFunc() {} or export const a = 1.

A default export looks like this: export default somethingAlreadyDefined.

A named export can be imported by itself using syntax that looks (and works) a bit like object destructuring: import { myFunc, a } from './some-module'.

It can also be imported as a namespace: import * as moduleY from './module-y' (can now use moduleY.myFunc() and moduleY.a).

A default export can only be imported with a default import: import whateverIsDefault from './moduleY'.

Theses 2 types of imports can also be mixed and matched, see import docs on MDN.

Intercepting JavaScript imports with jest.mock

When unit-testing, you may want to stub/mock out module(s) that have their own battery of unit tests.

In Jest, this is done with jest.mock('./path/of/module/to/mock', () => ({ /* fake module */ })).

In this case the CommonJS and ES6 Module mocks look quite similar. There are a few general gotchas.

First off, what you’re mocking with (2nd parameter of jest.mock) is a factory for the module. ie. it’s a function that returns a mock module object.

Second, if you want to reference a variable from the parent scope of jest.mock (you want to define your mock module instance for example), you need to prefix the variable name with mock. For example:

const mockDb = {
  get: jest.fn(),
  set: jest.fn()
};
const db = mockDb;

// This works
jest.mock('./db', () => mockDb);

// This doesn't work
jest.mock('./db', () => db);

Finally, you should call jest.mock before importing the module under test (which itself imports the module we just mocked).

In practice, Babel ESM -> CommonJS transpilation hoists the jest.mock call so it’s usually not an issue.

Intercept and mock a JavaScript CommonJS require/import

Given a module that has addTodo and getTodo functions exposed through a CommonJS interface as in the following snippet.

const db = require('./03.01-db');

const keyPrefix = 'todos';
const makeKey = key => `${keyPrefix}:${key}`;

let autoId = 1;

async function addTodo(todo) {
  const id = autoId++;
  const insertable = {
    ...todo,
    id
  };
  await db.set(makeKey(id), insertable);
}

function getTodo(id) {
  return db.get(makeKey(id));
}

module.exports = {
  addTodo,
  getTodo
};

We can intercept the 03.01-db import as follows

jest.mock('./03.01-db', () => ({
  get: jest.fn(),
  set: jest.fn()
}));

What we can then do is import the mocked DB module as well as the 03.01-lib-cjs module.

// db mocking code
const mockDb = require('./03.01-db');
const { addTodo, getTodo } = require('./03.01-lib-cjs');

We can proceed to write some tests for addTodo, where we check what db is called with. As we can see, jest.mock has set the DB module to be { get: jest.fn(), set: jest.fn() }.

// mocking & requires
test('CommonJS > addTodo > inserts with new id', async () => {
  await addTodo({ name: 'new todo' });
  expect(mockDb.set).toHaveBeenCalledWith('todos:1', {
    name: 'new todo',
    id: 1
  });
});

Similarly we can test getTodo by setting the return value of mockDb.get.

test('CommonJS > getTodo > returns output of db.get', async () => {
  mockDb.get.mockResolvedValueOnce({
    id: 1,
    name: 'todo-1'
  });

  const expected = {
    id: 1,
    name: 'todo-1'
  };
  const actual = await getTodo(1);

  expect(mockDb.get).toHaveBeenCalledWith('todos:1');
  expect(actual).toEqual(expected);
});

The tests pass as per the following output.

npx jest src/03.01-commonjs-mock.test.js
 PASS  src/03.01-commonjs-mock.test.js
  ✓ CommonJS > addTodo > inserts with new id (4ms)
  ✓ CommonJS > getTodo > returns output of db.get (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Next we’ll look at how to mock out an ES module.

Intercept and mock a JavaScript ES Module default export

To have ES modules support in Jest we’ll need to install babel-jest, babel-plugin-transform-es2015-modules-commonjs and @babel/core.

This can be done with the following command.

npm i --save babel-jest babel-plugin-transform-es2015-modules-commonjs @babel/core

We also need to configure Babel to compile ES2015 modules to commonjs, in the package.json we can do this by create a babel key and setting it as follows.

{
  "//": "// Other package.json properties",
  "babel": {
    "plugins": [
      "babel-plugin-transform-es2015-modules-commonjs"
    ]
  }
}

And we’ll need to configure Jest to apply the Babel transform to .js files using babel-jest, this can be done in the package.json using a jest key and setting it as follows.

{
  "//": "// Other package.json properties",
  "jest": {
    "transform": {
      "^.+\\.js$": "babel-jest"
    }
  }
}

Now given we have the ESM equivalent of the 03.01-lib module at src/03.01-lib.esm.js. Which has a addTodo, getTodo that are both default exported and named exports.

import db from './03.01-db';

const keyPrefix = 'todos';
const makeKey = key => `${keyPrefix}:${key}`;

let autoId = 1;

export async function addTodo(todo) {
  const id = autoId++;
  const insertable = {
    ...todo,
    id
  };
  await db.set(makeKey(id), insertable);
}

export function getTodo(id) {
  return db.get(makeKey(id));
}

export default {
  addTodo,
  getTodo
};

Much like for the CommonJS import interception, we’ll use jest.mock to intercept and manually set the mock for the module.

jest.mock('./03.01-db', () => ({
  get: jest.fn(),
  set: jest.fn()
}));

We can then import the mocked module and the module we want to test.

// module mocking
import mockDb from './03.01-db';
import lib from './03.01-lib.esm';

const {addTodo, getTodo} = lib;

We can proceed to writing the same tests as for the CommonJS version, that addTodo inserts with an incrementing id.

test('ESM Default Export > addTodo > inserts with new id', async () => {
  await addTodo({name: 'new todo'});
  expect(mockDb.set).toHaveBeenCalledWith('todos:1', {
    name: 'new todo',
    id: 1
  });
});

We can also write the test that sets the output of mockDb.get and checks it gets outputted correctly.

// imports & other tests.
test('ESM Default Export > getTodo > returns output of db.get', async () => {
  mockDb.get.mockResolvedValueOnce({
    id: 1,
    name: 'todo-1'
  });

  const expected = {
    id: 1,
    name: 'todo-1'
  };
  const actual = await getTodo(1);

  expect(mockDb.get).toHaveBeenCalledWith('todos:1');
  expect(actual).toEqual(expected);
});

The test output is as follows.

npx jest src/03.01-esm-default-mock.test.js
 PASS  src/03.01-esm-default-mock.test.js
  ✓ ESM Default Export > addTodo > inserts with new id (4ms)
  ✓ ESM Default Export > getTodo > returns output of db.get (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Intercept and mock a JavaScript ES Module named export

Given we have the ESM equivalent of the 03.01-lib module at src/03.01-lib.esm.js. Which has a addTodo, getTodo that are named exported as well as default exports.

import db from './03.01-db';

const keyPrefix = 'todos';
const makeKey = key => `${keyPrefix}:${key}`;

let autoId = 1;

export async function addTodo(todo) {
  const id = autoId++;
  const insertable = {
    ...todo,
    id
  };
  await db.set(makeKey(id), insertable);
}

export function getTodo(id) {
  return db.get(makeKey(id));
}

export default {
  addTodo,
  getTodo
};

Much like for the CommonJS import & ESM default export interception, we’ll use jest.mock to intercept and set the mock for the module.

jest.mock('./03.01-db', () => ({
  get: jest.fn(),
  set: jest.fn()
}));

We can then import the mocked module and the module we want to test.

// module mocking
import mockDb from './03.01-db';
import {addTodo, getTodo} from './03.01-lib.esm';

We can proceed to writing the same tests as for the CommonJS and ESM default exports version, that addTodo inserts with an incrementing id.

test('ESM Named Export > addTodo > inserts with new id', async () => {
  await addTodo({name: 'new todo'});
  expect(mockDb.set).toHaveBeenCalledWith('todos:1', {
    name: 'new todo',
    id: 1
  });
});

We can also write the test that sets the output of mockDb.get and checks it gets outputted correctly.

// imports & other tests.
test('ESM Named Export > getTodo > returns output of db.get', async () => {
  mockDb.get.mockResolvedValueOnce({
    id: 1,
    name: 'todo-1'
  });

  const expected = {
    id: 1,
    name: 'todo-1'
  };
  const actual = await getTodo(1);

  expect(mockDb.get).toHaveBeenCalledWith('todos:1');
  expect(actual).toEqual(expected);
});

The test output is as follows.

npx jest src/03.01-esm-named-mock.test.js
 PASS  src/03.01-esm-named-mock.test.js
  ✓ ESM Default Export > addTodo > inserts with new id (4ms)
  ✓ ESM Default Export > getTodo > returns output of db.get (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

In this section we looked at mocking CommonJS and ESM imports/exports as well as setting up Babel with Jest for ES module compilation.

In the next section we’ll look at how to rewrite a module in order to replace parts of it at runtime in order to test it more effectively.

Jump to table of contents