Over-mocking — when a test stops testing your code
It's possible to mock so much of a function's surroundings that the test only verifies the mocks behave the way you told them to — passing forever, regardless of whether the real code is actually correct.
3 min read
A test that can never fail, no matter what the code does
function processOrder(order, { validator, calculator, notifier }) {
if (!validator.isValid(order)) return { error: "invalid" };
const total = calculator.compute(order);
notifier.send(order.customerId, total);
return { total };
}
test("processes a valid order", () => {
const validator = { isValid: jest.fn(() => true) };
const calculator = { compute: jest.fn(() => 100) };
const notifier = { send: jest.fn() };
const result = processOrder({ items: [] }, { validator, calculator, notifier });
expect(result).toEqual({ total: 100 });
});Every single collaborator processOrder touches is mocked, and every mock is told exactly what to return. The assertion expect(result).toEqual({ total: 100 }) will pass regardless of what processOrder's actual logic does — because calculator.compute was told to return 100 no matter what, the 100 in the assertion isn't verifying processOrder's logic at all, it's verifying that a mock does what it was configured to do. This test can survive processOrder being rewritten to always return 0... as long as it still happens to call calculator.compute and blindly forward whatever comes back — which is a much weaker guarantee than the test's green checkmark implies.
The fix: mock only what genuinely needs to be a double, test the real logic directly
test("returns an error for an invalid order, without computing a total", () => {
const validator = { isValid: () => false };
const calculator = { compute: jest.fn() };
const notifier = { send: jest.fn() };
const result = processOrder({ items: [] }, { validator, calculator, notifier });
expect(result).toEqual({ error: "invalid" });
expect(calculator.compute).not.toHaveBeenCalled(); // verifies the SHORT-CIRCUIT actually happens
});
test("sends a notification with the computed total for a valid order", () => {
const validator = { isValid: () => true };
const calculator = { compute: () => 250 };
const notifier = { send: jest.fn() };
processOrder({ items: [{ price: 250 }] }, { validator, calculator, notifier });
expect(notifier.send).toHaveBeenCalledWith(undefined, 250); // real orchestration logic, actually exercised
});These two tests still use test doubles — calculator and notifier are legitimately worth doubling, since they'd otherwise mean real computation or a real notification being sent — but each test is actually exercising processOrder's real branching logic (does it skip computation on invalid input? does it forward the computed total to notifier correctly?) rather than just asserting that a chain of pre-programmed mocks did what they were told.
The "am I testing the mock or the code" check
A fast, reliable check: could this test still pass if the function under test were replaced with something that just returns whatever the mocks were told to return, ignoring its real logic entirely? If yes, the test isn't actually verifying the function's logic — it's verifying that mocks work, which they always do by construction. This is closely related to the earlier lesson's point about weak assertions: over-mocking is a specific, common way for a test to end up checking almost nothing, dressed up as a passing test with real-looking setup.
A rule of thumb: mock at the boundary, not the logic
The dependencies worth mocking are the ones that cross a real boundary out of your own code's control — a network call, a database, the filesystem, the current time, a payment provider. Internal logic that lives inside the same codebase — pure functions, small helper classes, the actual thing the test is trying to verify — generally shouldn't be mocked at all; calling it directly and checking its real output is both simpler to write and a much stronger guarantee than mocking it and asserting the mock behaved as configured.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's wrong with a test where every collaborator of the function under test is mocked and told exactly what to return?
2. What's the fast, reliable check for whether a test is testing the mocks instead of the real code?
3. What's the rule of thumb for deciding what's actually worth mocking?