TypeOfNaN

Partially mocking imports in Jest

Nick Scialli June 30, 2021🚀 4 minute read

jest framework

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!

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli is a software engineer at the U.S. Digital Service.