Partially mocking imports in Jest
Nick Scialli
June 30, 2021
Testing is all about isolating what you want to test. To do so, we might need to mock some functions exported from a module. Furthermore, we might want to not mock some of the functions exported from that module.
Consider the following contrived example. First, we define a module with a named export, api
, that performs a fetch
request. Then, we define a named export, returnSomething
that returns a string.
someModule.js
export const api = async (endpoint) => {
return await fetch(endpoint).then((res) => res.json());
};
export const returnSomething = () => {
return 'something';
};
Next, we import and call both functions in a getData
function that we’ll want to test later.
getData.js
import { api, returnSomething } from './someModule';
export const getData = async () => {
const data = await api('/some-endpoint');
return {
data: data,
somethingElse: returnSomething(),
};
};
Now the fun part: testing!
So now we want to test unit test our app
function: we’d like to test the return value of our getData
function. We start writing the test:
getData.test.js
import { getData } from './getData';
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: '???',
somethingElse: 'something',
});
});
});
But we don’t know what data
will be since our fetch
request is outside the boundary of our unit test. The good news is we can fix this we a Jest mock!
import { getData } from './getData';
jest.mock('./someModule', () => ({
api: () => Promise.resolve('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
});
However, if we try this, we still get errors!
TypeError: (0 , _utils.returnSomething) is not a function
This is because we’re mocking the entire someModule
module. Since we didn’t mock the returnSomething
function exported from it, it’s not defined!
To fix this, we can use jest.requireActual
to require the actual module and keep around the function we want:
jest.mock('./someModule', () => ({
returnSomething: jest.requireActual('./someModule').returnSomething,
api: () => Promise.resolve('foo'),
}));
And this works! But it becomes a pain if we have a lot of named exports from our module. So instead, let’s use the spread operator to copy over all of the actual exports from the module and then just overwrite the ones we want to overwrite:
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: () => Promise.resolve('foo'),
}));
Perfect! Our final code looks like this and we now have a nice, isolated, passing test:
import { getData } from './getData';
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: () => Promise.resolve('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
});
Making assertions about the mocked module
We might want to additionally make assertions with the mocked module. This is not a problem!
Let’s say we’d like to assert that the api
method is getting called. We can do this by making sure api
itself is a jest mock. We can still have it return a Promise that resolves with "foo"
if we’d like:
import { getData } from './getData';
// Make sure to import api to test it
import { api } from './someModule';
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: jest.fn().mockResolvedValue('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
it('call the api function', async () => {
const result = await getData();
expect(api).toHaveBeenCalled();
});
});
And that should work!
Note if api appears to be returning undefined
If you’re getting errors from this test because api
appears to be returning undefined
, it might be that jest, by default, clears mocks between tests.
To make sure this doesn’t happen, you’ll need to add the following to your jest configuration:
"jest": {
"resetMocks": false
}
And then, your tests should be passing! Just be sure to manually reset mocks between tests if you disable this options globally.
Conclusions
Testing is fun if you can figure out how to test what you want and mock out what you don’t!
Nick Scialli is a senior UI engineer at Microsoft.