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 interaction10
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 them11
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 original16
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.