/ #javascript #jest 

Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports

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

The example repository is available at github.com/HugoDF/mock-spy-module-import.

This post 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.

Table of Contents

For Context: 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

The full test and code under test is at examples/intercept-imports-cjs.

The relevant snippets are the following:

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

const mockDb = require('./db');
const {addTodo, getTodo} = require('./lib');

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

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

Intercept and mock a JavaScript ES Module default export

The full test and code under test is at examples/intercept-imports-esm-default.

import mockDb from './db';

import lib from './lib';

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

const {addTodo, getTodo} = lib;

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

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

Intercept and mock a JavaScript ES Module named export

The full test and code under test is at examples/intercept-imports-esm-named.

import * as mockDb from './db';

import {addTodo, getTodo} from './lib';

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

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

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

Spying/Stubbing calls to internal module functions with Jest

Warning: you should not be spying/stubbing module internals, that’s your test reaching into the implementation, which means test and code under test are tightly coupled

Concept: “calling through” (as opposed to mocking).

An internal/private/helper function that isn’t exported should be tested through its public interface, ie. not by calling it, since it’s not exported, but by calling the function that calls it.

Testing its functionality is the responsibility of the tests of the function(s) that consume said helper.

This is for the cases where:

  • you don’t have the time to extract the function but the complexity is too high to test through (from the function under test into the internal function)
    • solution: you should probably make time
  • the internal function belongs in said module but its complexity make it unwieldy to test through.
    • solution: you should probably extract it
  • the function is not strictly internal, it’s exported and unit tested, thereforce calling through would duplicate the tests.
    • solution: you should definitely extract it

In the following cases we’ll be looking to stub/mock/spy the internal makeKey function. This is purely for academic purposes since, we’ve shown in the section above how to test through the getTodo call.

In that situation we were testing expect(mockDb.get).toHaveBeenCalledWith('todos:1'); (see examples/intercept-imports-cjs/lib.jest-test.js). The generation of the todos:1 key is the functionality of makeKey, that’s an example of testing by calling through.

Mock/stub internal functions with Jest in a CommonJS module

A module where internal functions can’t be mocked

Code listing lifted from examples/spy-internal-calls-cjs/lib.fail.js.

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

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

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

module.exports = {
  makeKey,
  getTodo
};

As you can see when you run the examples/spy-internal-calls-cjs/lib.fail.jest-test.js tests, there’s no way to intercept calls to makeKey.

A module where internal function can be mocked

Code listing lifted from examples/spy-internal-calls-cjs/lib.js.

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

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

const lib = {
  // Could also define makeKey inline like so:
  // makeKey(key) { return `${keyPrefix}:${key}` },
  makeKey,
  getTodo(id) {
    return db.get(lib.makeKey(id));
  }
};

module.exports = lib;

Mocking/Spying the internal JavaScript function in a CommonJS module

Code listing lifted from examples/spy-internal-calls-cjs/lib.jest-test.js

// ignore setup code
test("CommonJS > Mocking destructured makeKey doesn't work", async () => {
  const mockMakeKey = jest.fn(() => 'mock-key');
  makeKey = mockMakeKey;
  await getTodo(1);
  expect(makeKey).not.toHaveBeenCalled();
  expect(mockDb.get).not.toHaveBeenCalledWith('mock-key');
});

test('CommonJS > Mocking lib.makeKey works', async () => {
  const mockMakeKey = jest.fn(() => 'mock-key');
  lib.makeKey = mockMakeKey;
  await getTodo(1);
  expect(mockMakeKey).toHaveBeenCalledWith(1);
  expect(mockDb.get).toHaveBeenCalledWith('mock-key');
});

test('CommonJS > Spying lib.makeKey works', async () => {
  const makeKeySpy = jest
    .spyOn(lib, 'makeKey')
    .mockImplementationOnce(() => 'mock-key');
  await getTodo(1);
  expect(makeKeySpy).toHaveBeenCalled();
  expect(mockDb.get).toHaveBeenCalledWith('mock-key');
});

From the above we can see that with the setup from the previous section (see examples/spy-internal-calls-cjs/lib.js), we’re able to both replace the implementation of lib.makeKey with a mock and spy on it.

We’re still unable to replace our reference to it. That’s because when we destructure lib to extract makeKey we create a copy of the reference ie. makeKey = newValue changes the implementation of the makeKey variable we have in our test file but doesn’t replace the behaviour of lib.makeKey (which is what getTodo is calling).

To illustrate:

const lib = require('./lib');
let {makeKey} = lib;

makeKey = 'something';

// `lib.makeKey` and `makeKey` are now different...

Mock/stub internal functions with Jest in an ES module

Difficulty of named exports

In the case of ES6 Modules, semantically, it’s quite difficult to set the code up in a way that would work with named exports, the following code doesn’t quite work:

import db from './db';

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

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

Code listing lifted from examples/spy-internal-calls-esm/lib.named-export.js, tests showing there’s no simple way to mock/spy on makeKey are at examples/spy-internal-calls-esm/lib.named-export.jest-test.js

Unmockeable ES modules with default exports

Code listing lifted from examples/spy-internal-calls-esm/lib.default-export.js.

import db from './db';

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

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

const lib = {
  makeKey,
  getTodo
};

export default lib;

Tests showing there’s no simple way to mock/spy on makeKey are at examples/spy-internal-calls-esm/lib.default-export.jest-test.js.

The reason this doesn’t work is the same as the CommonJS example, makeKey is directly referenced and that reference can’t be modified from outside of the module.

Anything attempting import it would make a copy and therefore wouldn’t modify the internal reference.

Mock/Spy setup internal functions in an ES module with Jest

Code listing lifted from examples/spy-internal-calls-esm/lib.js

import db from './db';

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

const lib = {
  // Could also define makeKey inline like so:
  // makeKey(key) { return `${keyPrefix}:${key}` },
  makeKey,
  getTodo(id) {
    return db.get(lib.makeKey(id));
  }
};

export default lib;

Passing tests for the above are at examples/spy-internal-calls-esm/lib.jest-test.js

Note, it would be possible to do something similar with named exports:

import db from './db';

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

export const lib = {
  // Could also define makeKey inline like so:
  // makeKey(key) { return `${keyPrefix}:${key}` },
  makeKey,
  getTodo(id) {
    return db.get(lib.makeKey(id));
  }
};

The key point is around exporting a lib object and referencing that same object when calling makeKey.

Mocking internals is the same with ESM/CommonJS

Being able to mock a part of a module is all about references.

If a function is calling another function using a reference that’s not accessible from outside of the module (more specifically from our the test), then it can’t be mocked.

Spy on imports or mock part of a module by “referencing the module”

Warning: this will cause you to change the way you write your code just to accomodate a specific type of testing.

This will break if anyone decides to get a copy of the module’s function instead of calling module.fn() directly.

CommonJS: Spy import/mock part of a module with Jest

Code listing lifted from examples/spy-module-cjs/lib.js.

Note how the db module is imported without destructuring and how any calls to it are done using db.method() calls.

const db = require('./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 are now able to spy on db.method using the following approach:

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

const {addTodo, getTodo} = require('./lib');

beforeEach(() => jest.clearAllMocks());

test('CommonJS > addTodo > inserts with new id', async () => {
  const dbSetSpy = jest.spyOn(db, 'set').mockImplementation(() => {});
  await addTodo({name: 'new todo'});
  expect(dbSetSpy).toHaveBeenCalledWith('todos:1', {name: 'new todo', id: 1});
});

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

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

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

Notice how we’re not calling jest.mock(). Instead we’re mocking/spying only a specific function of the module when we need to by modifying the db module implementation.

ES6 Modules: Spy import/mock part of a module with Jest

Default exports

Assuming our db.js module exports in the following manner (see examples/spy-module-esm-default/db.js):

const data = {};

async function get(k) {
  return data[k];
}

async function set(k, v) {
  data[k] = v;
}

const db = {
  get,
  set
};

export default db;

We can then import it as follows (code listing lifted from examples/spy-module-esm-default/lib.js):

import db from './db';

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

let autoId = 1;

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

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

const lib = {
  addTodo,
  getTodo
};

export default lib;

Spying on the import/mocking part of the module becomes possible in the following fashion (full code at examples/spy-module-esm-default/lib.jest-test.js):

import db from './db';

import lib from './lib';

const {addTodo, getTodo} = lib;

beforeEach(() => jest.clearAllMocks());

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

test('ESM Default Export > getTodo > returns output of db.get', async () => {
  const dbGetSpy = jest.spyOn(db, 'get').mockResolvedValueOnce({
    id: 1,
    name: 'todo-1'
  });

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

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

Notice how we don’t mock the db module with a jest.mock() call. Again we spy on the method that we’re interested in stubbing/spying for a particular test.

We leverage mockImplementationOnce() to avoid calling the real function (which you might not always want to do).

Named exports + “import * as alias from ‘module-name’”

Note: I’ve not read the full spec, the fact that this works might be a quirk of the Babel ES2015 module transpilation

Assuming we’ve defined db.js as follows (using named exports, see the file at examples/spy-module-esm-named/db.js):

const data = {};

export async function get(k) {
  return data[k];
}

export async function set(k, v) {
  data[k] = v;
}

We can import all the named exports under an alias with import * as db from './db' (code listing lifted from examples/spy-module-esm-named/lib.js):

import * as db from './db';

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

let autoId = 1;

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

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

The calls to db.set and db.get can be spied/mocked using the following approach (full code test file at examples/spy-module-esm-named/lib.jest-test.js):

import * as db from './db';

import {addTodo, getTodo} from './lib';

beforeEach(() => jest.clearAllMocks());

test('ESM named export > addTodo > inserts with new id', async () => {
  const dbSetSpy = jest.spyOn(db, 'set').mockImplementationOnce(() => {});
  await addTodo({name: 'new todo'});
  expect(dbSetSpy).toHaveBeenCalledWith('todos:1', {name: 'new todo', id: 1});
});

test('ESM named export > getTodo > returns output of db.get', async () => {
  const dbGetSpy = jest.spyOn(db, 'get').mockResolvedValueOnce({
    id: 1,
    name: 'todo-1'
  });

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

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

Partial module mocking with jest.requireActual

It’s possible to require/import and expose the real module while using jest.mock.

jest.requireActual(moduleName)

Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not

Jest documentation

The case where you would want to mock something partially is if you have a module that exposes both constants, pure functions and non-pure functions (that usually do I/O).

You’ll want to mock the operations that do I/O most of the time, the pure/business logic functions some of the time and the constants very seldom.

Here’s an example module that we might want to mock, notifications.js:

const OPERATIONS = {
  SEND_EMAIL: 'SEND_EMAIL',
  SEND_PUSH_NOTIFICATION: 'SEND_PUSH_NOTIFICATION'
};

function createEmailNotification(to, subject, content) {
  return {
    type: SEND_EMAIL,
    payload: {
      to,
      subject,
      content
    }
  };
}
function createPushNotification(to, title, content) {
  return {
    type: SEND_PUSH_NOTIFICATION,
    payload: {
      to,
      title,
      content
    }
  };
}

async function sendNotification(action) {
  // send something to an API
}

module.exports = {
  OPERATIONS,
  createEmailNotification,
  createPushNotification,
  sendNotification
};

Here’s how we’re likely to want to mock it:

jest.mock('./notifications', () => ({
  ...jest.requireActual('./notifications'),
  sendNotification: jest.fn(async () => {}),
));

In our test we are then able to access the real OPERATIONS, createEmailNotification and createPushNotification.

jest.mock('./notifications', () => ({
  ...jest.requireActual('./notifications'),
  sendNotification: jest.fn(async () => {}),
));

const { OPERATIONS, createEmailNotification, createPushNotification } = require('./notifications');

test('access tests', () => {
  expect(OPERATIONS).toEqual({
    SEND_EMAIL: 'SEND_EMAIL',
    SEND_PUSH_NOTIFICATION: 'SEND_PUSH_NOTIFICATION'
  });
  expect(createEmailNotification(
    '[email protected]',
    'new email notification',
    'This is an email notification'
  )).toEqual({
    type: 'SEND_EMAIL',
    to: '[email protected]',
    subject: 'new email notification',
    content: 'This is an email notification'
  });
  expect(createPushNotification(
    '[email protected]',
    'new push notification',
    'This is a push notification'
  )).toEqual({
    type: 'SEND_PUSH_NOTIFICATION',
    to: '[email protected]',
    title: 'new push notification',
    content: 'This is a push notification'
  })
})

Further Reading

The repository with examples is at github.com/HugoDF/mock-spy-module-import.

See more Testing and Jest posts on Code with Hugo.

You can find more Jest/testing/JavaScript content in the Enteprise Node.js and JavaScript newsletter archives.

unsplash-logoChris Becker

Looking for a new job? Take Triplebyte’s quiz and have top tech companies pitch you!

Author

Hugo Di Francesco

A Software Engineer who is big on Node.js, queues and Vue(s). Co-author of "Professional JavaScript" with Packt. He shares practical JavaScript tips for the developer who wants to get things done on Code with Hugo. University College London (UCL), MEng Mathematical Computation Graduate.

Get Testing Superpowers with these Underused Jest Features

Subscribe for free resources that turbocharge your Jest tests and a discount on the "Advanced Jest Handbook"