3.3 Spying/Stubbing calls to internal module functions with Jest

In principle, 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

The relevant concept is “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');. 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

Let’s create a src/03.03-lib-no-mock.cjs.js where we define makeKey and getTodo.

There’s no way to intercept calls to makeKey in the module, changing the makeKey reference won’t update the internal reference in the module itself.

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

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

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

module.exports = {
  makeKey,
  getTodo
};

A module where internal function can be mocked

In order to mock the makeKey function, we’ll need to make sure that getTodo references it from a place we can change it.

To do that we’ll create a lib object, set both makeKey and getTodo as properties of the lib object. getTodo will call lib.makeKey, in this fashion, we’ll be able to intercept its calls to makeKey.

const db = require('./03.03-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

We’ll create a test that will spy on lib.makeKey in a src/03.03-lib.cjs.test.js.

The setup code is as follows, we need to mock 03.03-db.js module and then import the 03.03-lib-mockable.cjs.js module and store it under lib.

jest.mock('./03.03-db');

const mockDb = require('./03.03-db');
const lib = require('./03.03-lib-mockable.cjs.js');

We’ll also destructure makeKey and getTodo from lib to illustrate that destructuring the import won’t work since we’re working with references.

let {makeKey, getTodo} = lib;

We can show that the destructured version won’t be mockable:

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

We can show that stubbing lib.makeKey works with the following test.

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

We can also spy on lib.makeKey and set a mockImplementation.

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, 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).

The tests pass as expected.

npx jest src/03.03-lib.cjs.test.js
 PASS  src/03.03-lib.cjs.test.js
  ✓ CommonJS > Mocking destructured makeKey doesn’t work (4ms)
  ✓ CommonJS > Mocking lib.makeKey works (1ms)
  ✓ CommonJS > Spying lib.makeKey works (1ms)

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

Mock/stub internal functions with Jest in an ES module

In the same way we mocked internals on CommonJS modules, we can mock them in ESM modules.

There’s a few caveats.

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

There’s no way to mock makeKey since it’s exported directly, there’s no wrapper for it.

The same is true of the following, since there’s no simple way to mock/spy on makeKey, for the same reasons 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.

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;

In order to mock module internals with an ESM export, we’ll need to build a lib object and then export it. Taking care to make getTodo reference lib.makeKey and not the direct makeKey reference.

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;

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.

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.

We’ve now seen how to mock module internals by exporting a library/module object and running any internal references to it.

In the next section we’ll look at testing asynchronous code with Jest.

Jump to table of contents