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
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
const spy = jest.spyOn(rates, 'gbp').mockResolvedValue(0.79);
await invoice.total();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore(); // or set restoreMocks: true in the configPreferable to module mocking whenever you have a reference to the object, because it is explicit, scoped and restorable.
#jest.mock() — replace a whole module
// 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:
// 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.
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
// 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:
// 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
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 ↗- TypeScript
typescript/src/tools/jest-mocking
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- JestThe default JavaScript test runner — configuration, projects, snapshot testing, fake timers, and the choices that keep a large Jest suite fast.
- Test DoublesDummy, stub, spy, mock and fake — Meszaros's five kinds of test double, what each is for, and why using the words precisely makes code reviews shorter.
- Mocking FrameworksMoq, NSubstitute, Mockito, unittest.mock, Sinon and the rest — what each ecosystem's mocking library does well, and the failure modes they share.
- Unit Testing with MocksThe solitarist approach — isolate the unit behind test doubles. What it buys, the coupling it creates, and the three rules that keep a mock-heavy suite maintainable.
- Testing ReactComponents, hooks, context, server components and async state — what to test in a React application, what to leave alone, and how to avoid act warnings.
- Writing Tests in TypeScriptThe TypeScript testing toolchain — Jest or Vitest, typed doubles, async idioms, and the type-level tricks that make tests both safer and more readable.