A spy watches a real function and records how it was called, without necessarily changing what it does. A mock replaces a function or module with a stand-in you fully control — its return value, its behavior, whether it throws.
// jest.fn() creates a bare mock function — records calls, returns undefined unless configured
const onSave = jest.fn();
onSave("draft-1");
expect(onSave).toHaveBeenCalledWith("draft-1");
expect(onSave).toHaveBeenCalledTimes(1);
// Configuring what a mock returns
const getConfig = jest.fn().mockReturnValue({ retries: 3 });
const getConfigOnce = jest.fn().mockReturnValueOnce("first call only").mockReturnValue("every call after");
// jest.spyOn wraps an EXISTING method, so you can still call through to the real one
const spy = jest.spyOn(Math, "random").mockReturnValue(0.5);
// ...
spy.mockRestore(); // put the real Math.random back — important for test isolation
// api.js
export function fetchUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
// api.test.js
jest.mock("./api"); // auto-mocks every export as a jest.fn()
import { fetchUser } from "./api";
test("loadProfile shows the user's name", async () => {
fetchUser.mockResolvedValue({ id: 1, name: "Ana" });
const profile = await loadProfile(1); // loadProfile calls the real fetchUser internally
expect(profile.displayName).toBe("Ana");
});
jest.mock("./api") replaces the entire module with auto-mocked functions for the whole test file, which is exactly what you want when testing loadProfile in isolation from the real network call fetchUser would otherwise make. For a mock too specific or elaborate for the auto-mock (custom class behavior, stateful fakes), Jest also supports manual mocks — a file at __mocks__/api.js next to the real module, with your own hand-written implementation, that jest.mock("./api") picks up automatically instead of generating one.
Mocking exists to cut a test off from something genuinely outside its control — a real network call, the current date, a payment provider's API, a slow database. That's the right use: it makes a test fast, deterministic, and independent of infrastructure that shouldn't need to be running just to check one function's logic.
It stops being useful the moment you mock the very thing the test claims to verify:
// This test provides almost no real signal.
// It mocks calculateShipping, then asserts calculateShipping was called —
// it never runs the actual shipping calculation logic at all.
jest.mock("./shipping");
test("checkout calls calculateShipping (weak)", () => {
processCheckout(cart);
expect(calculateShipping).toHaveBeenCalled();
});
That test will stay green forever, even if calculateShipping is completely broken, because the mock replaced it before any real logic ever ran. A useful version either lets the real calculateShipping run (if it's fast and pure — a good candidate for a plain unit test with no mocking at all) or asserts on an actual outcome that depends on its real result, not merely on the fact that it got invoked.
A practical rule: mock things you don't own or that are genuinely slow/nondeterministic (a third-party API, the system clock, the filesystem). Be far more hesitant to mock your own business logic just to make a test faster or more isolated — that's usually a sign the function under test is doing too much, or that the test would be better written one layer down, directly against the logic it actually cares about.