This episode opens up Jest's mocking engine: creating fake functions with jest.fn and jest.spyOn, distinguishing manual mocks from automatic mocks, replacing modules with jest.mock, and controlling implementations and reset behavior.

A good unit test tests the unit in isolation. That's exactly what mocking is for: replacing real dependencies with fakes so the test focuses on the unit's own behavior rather than its dependencies' behavior. Episode 5 opens up Jest's mocking engine from four angles: mock functions, spying, module mocking, and implementation control.
Understanding mocking is the difference between fragile tests and resilient tests. A misplaced mock makes a test test nothing at all, while a well-placed mock makes the suite fast, deterministic, and free of external network calls.
jest.fn() creates a trackable fake function: how many times it was called, with which arguments, and what it returned:
test("jest.fn records calls", () => {
const call = jest.fn();
call("hello");
call("world");
expect(call).toHaveBeenCalledTimes(2);
expect(call).toHaveBeenCalledWith("hello");
expect(call).toHaveBeenLastCalledWith("world");
});jest.fn() with no arguments creates a function that returns undefined. expect(call).toHaveBeenCalledWith("hello") checks one of the previous calls, while toHaveBeenLastCalledWith checks the last call.
You can set a return value or a full implementation:
test("mock with a return value", () => {
const calc = jest.fn(() => 42);
expect(calc()).toBe(42);
calc.mockReturnValue(7);
expect(calc()).toBe(7);
calc.mockImplementation((x) => x * 2);
expect(calc(5)).toBe(10);
});There are three ways to set behavior: mockReturnValue for a fixed value, mockResolvedValue for promises, and mockImplementation for a full function. Choose the simplest one that meets your needs.
jest.spyOn(object, "method") replaces an object's method with a fake while still recording its calls. Unlike jest.fn, a spy calls the original implementation by default:
const service = {
send: (message) => `sending: ${message}`,
};
test("spy calls the original implementation", () => {
const spy = jest.spyOn(service, "send");
const result = service.send("hello");
expect(result).toBe("sending: hello");
expect(spy).toHaveBeenCalledWith("hello");
spy.mockRestore();
});jest.spyOn(service, "send") temporarily replaces the method; the call service.send("hello") still runs the original logic because the spy passes through to the implementation by default. spy.mockRestore() restores the method to its original state.
__mocks__ directory, giving you full control over the fake's shape and behavior.jest.fn that returns undefined.Automatic mocks are quick to create but often return undefined, which doesn't match what the code expects. Manual mocks take more effort but are far more realistic. For external modules like HTTP clients, manual mocks are usually the better choice.
__mocks__/
http-client.js
src/
user.js
user.test.jsFill __mocks__/http-client.js with a complete fake implementation, then call jest.mock in the test to activate it.
jest.mock("../path/module") tells Jest to use a fake version of that module for all imports in the test file. If a manual mock exists in __mocks__, that one is used; otherwise, Jest creates an automatic mock:
jest.mock("../http-client", () => ({
get: jest.fn(),
post: jest.fn(),
}));
import { get, post } from "../http-client";
test("http-client is mocked", () => {
get.mockResolvedValue({ data: "ok" });
return expect(get("/users")).resolves.toEqual({ data: "ok" });
});jest.mock("../http-client", () => ({ get: jest.fn() })) replaces the whole module with the factory you provide. The advantage: the real module doesn't need to exist — tests can run before the real module is even written.
Mock reset behavior often confuses beginners. Here's the difference:
mockClear(): removes call records, but keeps the implementation.mockReset(): removes records and restores the implementation to its empty default.mockRestore(): restores the original method for spies, while also cleaning up.afterEach(() => {
jest.clearAllMocks();
});afterEach(() => { jest.clearAllMocks(); }) clears the call records of all mocks after each test. Combined with the clearMocks: true config option, this keeps mocks clean without touching their implementations.
Episode 5 covered mocking and spying: jest.fn() for trackable fake functions, jest.spyOn() for observing objects without removing implementations, jest.mock() for replacing modules, plus an understanding of manual mocks versus automatic mocks and the right reset strategies.
Key takeaways:
jest.fn() for mock functions; jest.spyOn() for objects whose original is still called.mockReturnValue, mockResolvedValue, and mockImplementation control mock behavior.jest.mock() replaces a module across an entire test file.mockClear, mockReset, and mockRestore each play a different role — understand each one.afterEach with jest.clearAllMocks keeps mocks clean between tests.In the next episode, episode 6, we'll tackle asynchronous testing — testing promises with resolves and rejects, the async/await pattern, callbacks with done, and controlling timers with fake timers. This is essential to master because most modern applications are full of async operations.