All topics
Testingintermediate

Mocking Functions and Modules

Replacing real dependencies with controlled fake implementations during tests to isolate the code under test and verify how it interacts with its dependencies.

A mock replaces a real dependency so a test can control it and check how it was used.

Mocking is like rehearsing a play with a stand-in prop gun instead of a real one — the actor still practices exactly the same motions and cues (the interaction/call pattern being verified) without any of the real risk, cost, or unpredictability a live prop would introduce, and the director can specifically check whether the actor pulled the trigger at the right moment, regardless of what a real gun would have actually done.

Key Concepts

1
    const send = jest.fn();
    send.mockReturnValue(true);
    send.mockResolvedValue({ id: 1 });
    send.mockImplementation((x) => x * 2);
2
    expect(send).toHaveBeenCalledWith('a@b.com');
    expect(send).toHaveBeenCalledTimes(1);
3
Mocking a module.
    jest.mock('./emailService');
    import { sendEmail } from './emailService';
    sendEmail.mockResolvedValue(true);
4
Which is the approach to avoid where you can. Module mocking couples the test to the file layout, so moving a file breaks tests that never referenced its contents.
5
Injection is better.
    // instead of importing the dependency
    function createUserService(emailService) { ... }
6
    // the test passes a fake -- no framework needed
    const service = createUserService({ send: jest.fn() });
7
That is the practical argument for dependency injection, and it is why the two topics belong together.
8
Spy, stub and mock, which are used loosely and mean different things:
9
    spy    records calls; the real thing still runs
    stub   returns a canned value; records nothing
    mock   returns canned values AND asserts on the interaction
10
What to mock.
    network calls        yes -- slow, flaky, and they hit real systems
    the database         in unit tests; use a real one in integration
    time                 yes -- jest.useFakeTimers()
    randomness           yes -- otherwise the test is not deterministic
    pure functions       no -- just call them
11
Over-mocking is the failure mode. A test where everything is mocked verifies that the mocks were called, not that the code works.
12
    // this passes even if the implementation is wrong
    expect(mockA).toHaveBeenCalled();
    expect(mockB).toHaveBeenCalled();
13
The sign is a test that keeps passing after you break the thing it tests.
14
Reset between tests.
    afterEach(() => jest.clearAllMocks());
15
    clearAllMocks    call history
    resetAllMocks    history and implementations
    restoreAllMocks  puts spies back to the original
16
Leftover state from one test affecting the next is the classic cause of order-dependent failures.
17
What the interviewer is probing.1. "What should you mock?" Probing: the boundary. Stalls: "Everything." Moves up: I/O, time, randomness and network — not pure functions you own, where mocking tests the mock.
18
2. "What is the sign of over-mocking?" Probing: the symptom. Stalls: "Too many lines." Moves up: the test still passes after you break the logic it supposedly covers, because it only asserts that mocks were called.
19
3. "Spy, stub or mock?" Probing: the vocabulary. Stalls: "Interchangeable." Moves up: a spy records calls while the real code runs, a stub returns a canned value, a mock also asserts on the interaction.
20
4. "When is a fake better than a mock?" Probing: the alternative. Stalls: "Never." *Moves up:* a small in-memory implementation gives realistic behaviour without re-stubbing for every case — an in-memory repository, for instance.