Skip to content
End To End Tester

Jest Mocking

Module mocks, spies, manual mocks and timer control — how Jest's mocking works, and the patterns that keep it from taking over a suite.

2 min read · updated 19 September 2026

Jest's mocking is powerful, convenient and the most common way a JavaScript suite becomes unmaintainable. The mechanics are worth knowing precisely, and so is the discipline about when not to use them.

#Three levels

#jest.fn() — a function double

typescript
const send = jest.fn().mockResolvedValue({ id: 'msg_1' });

await notify(customer, send);

expect(send).toHaveBeenCalledWith(
  expect.objectContaining({ to: customer.email, subject: expect.stringMatching(/order/i) })
);

Plain, local, no magic. When you can pass the double in as an argument, do — it is the least coupled option available. See dependency injection.

#jest.spyOn() — wrap an existing method

typescript
const spy = jest.spyOn(rates, 'gbp').mockResolvedValue(0.79);

await invoice.total();

expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();          // or set restoreMocks: true in the config

Preferable to module mocking whenever you have a reference to the object, because it is explicit, scoped and restorable.

#jest.mock() — replace a whole module

typescript
// Hoisted above the imports by Jest's transform. The factory therefore
// cannot close over anything declared later in the file.
jest.mock('./rates-client', () => ({
  fetchGbpRate: jest.fn()
}));

import { fetchGbpRate } from './rates-client';
import { totalInGbp } from './invoice';

const mockFetch = fetchGbpRate as jest.MockedFunction<typeof fetchGbpRate>;

beforeEach(() => mockFetch.mockReset());

it('converts using the current rate', async () => {
  mockFetch.mockResolvedValue(0.79);

  await expect(totalInGbp({ usdCents: 10_000 })).resolves.toBe(7_900);
});

The hoisting catches everyone once:

typescript
// ReferenceError: Cannot access 'rate' before initialization
const rate = 0.79;
jest.mock('./rates-client', () => ({ fetchGbpRate: () => rate }));

// Works: the `mock` prefix is on Jest's allowlist for hoisted factories.
const mockRate = 0.79;
jest.mock('./rates-client', () => ({ fetchGbpRate: () => mockRate }));

#Partial mocks

Usually you want one function replaced and the rest real.

typescript
jest.mock('./pricing', () => ({
  ...jest.requireActual('./pricing'),      // keep the real module
  fetchLiveRates: jest.fn()                // replace only the I/O
}));

This is the pattern to reach for by default. Replacing a whole module means the test no longer exercises any of the real logic in it, which is rarely what was intended.

#Manual mocks

A __mocks__ directory next to the module provides a reusable double.

lib/
  stripe.ts
  __mocks__/
    stripe.ts        ← used whenever a test calls jest.mock('./stripe')

Useful for a dependency doubled in twenty files. For node_modules, a __mocks__ directory at the project root is picked up automatically — which is convenient and occasionally surprising, so keep the list short.

#Config that prevents leaks

javascript
// jest.config.js
module.exports = {
  clearMocks: true,     // reset calls between tests
  restoreMocks: true,   // undo spyOn automatically
  resetModules: true    // fresh module registry per test file
};

Without these, a spy from one test survives into the next, and failures depend on the order files happen to run in — an order-dependency bug of exactly the kind described in parallel test execution.

#What to mock, and what not to

Mock: the network, the filesystem, the clock, randomness, third-party SDKs, anything slow or non-deterministic.

Do not mock: your own pure functions, value objects, or the module you are testing. A test that mocks four of its own modules is testing the wiring diagram, not the behaviour — see unit testing without mocks.

Never mock fetch. It is the most common mock in JavaScript and the least useful. Replacing fetch skips your client code, your error mapping, your retry logic and your serialization — exactly the parts most likely to be wrong. Intercept at the network layer instead:

typescript
// MSW. The real client runs; only the far side of the wire is fabricated.
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('https://api.example.com/rates/gbp', () => HttpResponse.json({ rate: 0.79 }))
);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

it('surfaces a rate limit rather than retrying forever', async () => {
  server.use(
    http.get('https://api.example.com/rates/gbp', () =>
      HttpResponse.json({}, { status: 429, headers: { 'retry-after': '60' } })
    )
  );

  await expect(fetchGbpRate()).rejects.toMatchObject({ name: 'RateLimited' });
});

More on this approach in integration testing with stubs.

#Assertions on mocks, used sparingly

typescript
expect(send).toHaveBeenCalledTimes(1);
expect(send).toHaveBeenCalledWith(expect.objectContaining({ to: '[email protected]' }));
expect(send).toHaveBeenNthCalledWith(2, expect.anything());
expect(send.mock.calls[0][0].subject).toMatch(/refund/i);

Every one of these couples the test to how the code works. That is sometimes exactly right — sending the email is the behaviour — and often a substitute for an assertion about an outcome that would be more durable. The question from test doubles applies: if the implementation changed shape but the behaviour did not, would this test still pass?

Common questions

Why is jest.mock hoisted?
Module mocks must be registered before the module under test imports its dependency, and imports are evaluated before the body of the file. Jest's transform moves jest.mock calls above the imports to make that work — which is why a jest.mock factory cannot reference a variable declared later in the file.
Should I mock fetch or use MSW?
Use MSW. Mocking fetch replaces the whole HTTP layer with an assumption; MSW intercepts at the network boundary, so your real client code, serialization and error handling all run. It also gives you one set of handlers shared by unit tests, component tests and the dev server.
How do I stop mocks leaking between tests?
Set restoreMocks and clearMocks to true in the config. Without them a spy installed in one test survives into the next, which produces failures that depend on file order and are painful to diagnose.

Runnable samples for this page

last test results ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?