How to use Jest mocks

For better or worse, my life has become infested by Jest mocks.

I come from the land of dependency injection (I even gave a talk once!), but whenever I talk about dependency injection to JavaScript developers I get this reaction:

Meme of Black Panther saying "we don't do that here"

Instead, mocking is king. Unfortunately, I’ve found Jests’s API to be confusing and the docs largely unhelpful.

I thought I could get by winging it but that never works in the long run, so I finally sat down and studied how Jest mocking actually works. Here's the notes from my research.

(I’m not aiming to write a complete guide here, just covering what I use regularly.)

Mocking modules

The classic example of needing to mock is when you want to test a component that depends on an API. You want to test the business logic, but you don't want to actually make network calls to an external service:

A dependency graph showing that the "test" module depends on the "business logic" module, which then depensd on the "external API" module

This is where jest.mock() comes into play. Calling it replaces the real module with a mock version:

import 'logic';

jest.mock('api');

test('logic foo', () => { 
  expect(logic.foo()).etc();
});

Calling logic.foo() normally results in a real API call, but since I mocked api, it doesn't.

Because mock modules must be set up before you start importing modules, Jest will “hoist” all jest.mock() calls so they execute first. As such, it doesn’t matter where you put your calls to jest.mock() (which honestly spooks me a bit).

(Mock hoisting is why you can’t, for example, use constants in your mocks. While the constant might be defined in the file before calling jest.mock(), the execution order flips them the other way around!)

Automock

What is returned from a mocked module, anyways?

By default, Jest “automocks” modules, replacing its properties with mockable alternatives. For example, it replaces any function with a mock function (aka jest.fn()). It’ll walk object trees, so if you’ve got an object with a function property, it’ll mock that function as well.

Not all module properties are mocked, though. For example, constants (e.g. strings or numbers) are unaffected by automock. I couldn’t find any docs mapping type → mock behavior, so some trial-and-error may be required for unusual circumstances.

(Warning: “automock” is an overloaded term in Jest; it’s used both to describe what happens to a mocked module by default, but is also the name of the automock feature.)

Mocking behavior

Automock is all well and good for keeping the tests from crashing, but sometimes you need to replace behavior as well (such as returning a stub value).

Let's make our mocked API return a sample response:

api.get.mockReturnValue('hello');
expect(api.get()).toEqual('hello');

Normally api.get() would query the API; here, we force it to return "hello" instead.

There’s a bunch of ways to mock return values, such as mockReturnValue(), mockResolvedValue(), and mockImplementation(). It depends on whether you want to return a constant, a promise, or add some logic to the mock.

If you’re using TypeScript, make sure you wrap your mocked function with jest.mocked() before you use it!

jest.mocked(api.get).mockReturnValue('hello');

Calling jest.mocked() adds type information. Without it, you'll get compiler errors & sadness.

Partially mocking modules

What if you only want to mock some (but not all) of the module?

In that case, jest.mock() has a second parameter: a factory function. When provided, the module is mocked with its output instead of using automock.

The trick to partial mocks is to combine jest.requireActual() (which imports the real implementation of a module) with explicit mocking:

jest.mock('api', () => {
  return {
    ...jest.requireActual('api'),
    get: jest.fn().mockReturnValue('hello'),
  };
});

We’ve mocked api.get() but the rest of the api module uses the actual implementation.

The factory function allows for all sorts of shenanigans, such as returning a module which doesn’t match the spec of the original module at all. That sucks, so I only use factory functions when needed.

Cleaning mocks

Reusing a mock usually means cleaning up between tests. There are three levels of cleaning:

  • Clear - clears mock usage metadata (e.g., how many times was this mock function called).
  • Reset - does the above plus replaces the mocked function w/ the default empty function (i.e. undoes calls like mockReturnValue()).
  • Restore - does all the above plus restores the function to its original (real) implementation.

You can clean up individual functions (using myMock.mockClear(), myMock.mockReset(), or myMock.mockRestore()) or you can apply it to all mocks at once (using jest.clearAllMocks(), jest.resetAllMocks(), and jest.restoreAllMocks()).

You can also call jest.resetModules() to completely reset the modules in the cache, in case there’s some local state in a module that you need to clear between tests.