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.