Mock ES Modules in Jest

May 22, 20212 min read

Overview

Mocking ES modules in Jest could be pretty tricky when you are mixing different kinds of exports. In most frontend codebases, usually you will see a mixture of named and default exports, and a mixture of constant and function exports. This article will go over these different types of exports and how to mock them in Jest.

Named function export

This is one of the easiest kinds of exports to mock and you can easily mock the function implementation with Jest's automock capability.

Example

// foo.js
export const foo = () => 'foo';
// foo.test.js
jest.mock('./foo');
import { foo } from './foo';

foo.mockImplementation(() => 'bar');

foo(); // 'bar'

Explain

When module foo is mocked by jest.mock, all the exported functions will be replaced by jest.fn(), which allows you to easily mock its implementation with mockImplementation method.

Typescript

When using Typescript, to hint Typescript compiler that foo is a mocked function, you could do:

// foo.test.ts
(foo as jest.Mock).mockImplementation(() => 'bar');

Named constant export

Named constant export is slightly harder to mock compared to named function export because constants don't have return values and therefore don't have mockImplementation method. One way to mock named constant export is to mock the whole module with jest.mock, import the whole module with a new namespace, and then assign a new value to the named export.

Example

// foo.js
export const foo = 'foo';
// foo.test.js
jest.mock('./foo');
import * as Foo from './foo';

Foo.foo = 'bar';

foo; // 'bar';

Explain

By assigning new value to an exported member of a mocked module, we are essentially swapping out the value in the last minute before this value is used in other places. Usually this assignment will be caught by linting rule no-import-assign. If you include the following snippet in the test, it should be save to disable this rule temporarily.

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

Default function export

Default function export can be mocked with jest.mock along with an extra field __esModule.

Example

// foo.js
export default () => 'foo';
// foo.test.js
jest.mock('./foo', () => ({
    __esModule: true,
    default: jest.fn()
}))
import foo from './foo';

foo.mockImplementation(() => 'bar');

foo(); // 'bar'

Explain

By supplying second argument in jest.mock, we allow Jest to provide a module factory for our mocked module. And by setting __esModule, we are essentially telling Jest that this module is a ES module and therefore it has a "named" export called "default". Since functions can be mocked with jest.fn(), we are able to change the implementation of mocked function later.

Default constant export

Default constant export is most limited in terms of flexibility in mocking. The way to mock default constant export is very similar to mocking default function export, but we are only able to mock the value once.

Example

// foo.js
export default 'foo';
// foo.test.js
jest.mock('./foo', () => ({
    __esModule: true,
    default: 'bar'
}))
import foo from './foo';

foo; // 'bar'

Explain

Since default constant export is not re-assignable, we can only define its value at the time the module is mocked. Given its limited flexibility in testing, it is not recommended to use default constant exports.

Conclusion

From flexibility point of view, the best way to export is using named function export as it allows you to mock with mockImplementation automatically. If you know an exported member in a module is going to be mocked in other tests, it is better to export that member as a function, even when the return value will likely stay unchaged in actual practice.