Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports
Tags: javascript, testing, jest, webdev
Published: 2019-09-13 07:53:24

---
title: Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports
published: true
tags: JavaScript, testing, jest, webdev
canonical_url: https://codewithhugo.com/jest-mock-spy-module-import/
---

> 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](https://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.

## 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).

## Intercepting imports

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 🤷‍♀.

### The CommonJS case

The full test and code under test is at [examples/intercept-imports-cjs](https://github.com/HugoDF/mock-spy-module-import/tree/master/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);
});

```

### ES Modules default export

The full test and code under test is at [examples/intercept-imports-esm-default](https://github.com/HugoDF/mock-spy-module-import/tree/master/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);
});

```

### ES Modules named export

The full test and code under test is at [examples/intercept-imports-esm-named](https://github.com/HugoDF/mock-spy-module-import/tree/master/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

> 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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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**.

### CommonJS

#### A module where internal functions can’t be mocked

Code listing lifted from [examples/spy-internal-calls-cjs/lib.fail.js](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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 function

Code listing lifted from [examples/spy-internal-calls-cjs/lib.jest-test.js](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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...

```

### ES Modules

#### 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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/examples/spy-internal-calls-esm/lib.named-export.jest-test.js)

#### Unmockeable modules with default exports

Code listing lifted from [examples/spy-internal-calls-esm/lib.default-export.js](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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 with ESM

Code listing lifted from [examples/spy-internal-calls-esm/lib.js](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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](https://github.com/HugoDF/mock-spy-module-import/tree/master/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);
});

```

## Further Reading

The repository with examples is at [github.com/HugoDF/mock-spy-module-import](https://github.com/HugoDF/mock-spy-module-import).

See more [Testing](https://codewithhugo.com/tags/testing) and [Jest](https://codewithhugo.com/tags/jest) posts on Code with Hugo.

You can find more Jest/testing/JavaScript content in the [Enteprise Node.js and JavaScript newsletter archives](https://buttondown.email/hugo/archive).

[<svg xmlns="http://www.w3.org/2000/svg" style="height:12px;width:auto;position:relative;vertical-align:middle;top:-2px;fill:white" viewbox="0 0 32 32"><title>unsplash-logo</title>
<path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z"></path></svg>Chris Becker](https://unsplash.com/@chrisbecker?utm_medium=referral&utm_campaign=photographer-credit&utm_content=creditBadge "Download free do whatever you want high-resolution photos from Chris Becker")