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.