ConstructiCat Logo
CodeBust.
Browse section ▾

Excessive Mocking.

A test wires up so many mock objects and stubbed interactions that the mock setup dwarfs the actual verification, so the test ends up exercising the mocks rather than real behavior.

##Signs and Symptoms

A test is mostly arrange: long blocks of mock creation, when(...).thenReturn(...) / mockReturnValue(...) and verify(...) lines, with little real code under test. Tell-tale signs:

  • More mock plumbing than assertions. The setup is many lines; the "act" is one call; the "assert" checks that mocks were called rather than that an outcome is correct.
  • Mocking things you own. Domain objects, value objects, or pure-logic collaborators are mocked instead of only the external boundaries (network, DB, clock, filesystem, third-party).
  • Mock chains / "train wrecks." A mock returns another mock that returns another mock, mirroring the production call graph.
  • Interaction-only verification. The test asserts toHaveBeenCalledWith(...) on every collaborator and never asserts the returned value or resulting state.
  • Brittle on refactor. Reordering internal calls or extracting a method breaks many tests even though behavior is unchanged.
  • Five-plus mocks just to instantiate the SUT — usually a sign the SUT itself has too many dependencies.
test('places order', () => {
  const inventory = { check: jest.fn().mockReturnValue(true) };
  const pricing   = { quote: jest.fn().mockReturnValue(42) };
  const tax       = { calc:  jest.fn().mockReturnValue(4.2) };
  const wallet    = { charge: jest.fn().mockReturnValue({ ok: true }) };
  const ledger    = { record: jest.fn() };
  const emailer   = { send: jest.fn() };
  const audit     = { log: jest.fn() };
  const clock     = { now: jest.fn().mockReturnValue(0) };

  const svc = new OrderService(inventory, pricing, tax, wallet, ledger, emailer, audit, clock);
  svc.place(cart);

  // asserts the mocks were called — not that the order is right
  expect(inventory.check).toHaveBeenCalled();
  expect(pricing.quote).toHaveBeenCalled();
  expect(wallet.charge).toHaveBeenCalledWith(46.2);
  expect(ledger.record).toHaveBeenCalled();
});

##Reasons for the Problem

Why it happens

  • The SUT has too many collaborators. Excessive mocking is usually a design signal: a class with low cohesion and many dependencies forces every test to stand up all of them. As the catalog literature puts it, "if you need to mock five internal classes just to test one method, the method has too many dependencies — that's a design problem, not a testing problem."
  • Habit / "mock everything" reflex. Taking interaction-based ("London school") testing to an extreme, or reflexively mocking to avoid touching a DB or network, leads to mocking pure logic that could be tested directly.
  • Hard-to-build real objects. When real collaborators are awkward to construct, a mock feels easier than fixing the constructor or adding a fake.

Why it hurts

  • False confidence. Mocks encode your assumptions about a dependency. If the real implementation diverges, the test still passes — e.g. a stub that claims sum() returns only positive integers keeps a green test after the real method changes. Such tests can become tautological: they only verify that the mocks you wrote behave like the mocks you wrote. As Mockito's own docs warn, "if everything is mocked, are we really testing the production code?"
  • Brittleness / high maintenance. Verifying specific calls treats implementation details as the contract, so harmless refactors (call order, extracted helpers) break tests. This is Meszaros's Overspecified Software / Fragile Test.
  • Poor readability. Hundreds of lines of dependency configuration bury the one thing the test is about, much like a Mystery Guest — a reviewer can't tell what behavior is actually asserted.
  • Missed integration bugs. The real wiring between components is never exercised; bugs live precisely in the seams the mocks replaced. High coverage masks low quality.

##Treatment

Treat heavy mocking as feedback, then reduce the need to mock:

  1. Fix the design first. If you must mock 5+ collaborators to build the SUT, split responsibilities or reduce constructor dependencies. Fewer real dependencies means fewer mocks.
  2. Mock only at architecture boundaries. Mock the things you don't own (network, DB, clock, filesystem, third-party APIs); use real instances of your own domain and value objects.
  3. Extract a pure core (functional core / imperative shell). Move calculation/decision logic out of the I/O-heavy class so it can be tested with zero mocks, leaving a thin shell that needs only one or two boundary doubles.
  4. Prefer state verification over interaction verification. Assert the returned value or resulting state instead of verify(...)/toHaveBeenCalledWith(...) on every collaborator. Reserve interaction checks for the one side effect that genuinely matters.
  5. Use the simplest double that works. Replace elaborate mocks with stubs (just return values) or a single reusable in-memory fake instead of re-stubbing each method per test.
  6. Surface over-mocking dynamically. Enabling Mockito strict stubbing (the default MockitoExtension/MockitoJUnitRunner) throws UnnecessaryStubbingException for stubs that are configured but never used — a cheap way to find mocks you didn't need.
// BEFORE: 8 mocks, asserting calls
const pricing = { quote: jest.fn().mockReturnValue(42) };
const tax     = { calc:  jest.fn().mockReturnValue(4.2) };
// + 6 more mocks ...
expect(wallet.charge).toHaveBeenCalledWith(46.2);

// AFTER: pure logic tested directly — no mocks
expect(totalFor(cart, rates)).toBe(46.2);

// only the real boundary is faked; assert state, not calls
const wallet = new InMemoryWallet({ balance: 100 });
const svc = new OrderService(new InMemoryInventory(cart), wallet);
const order = svc.place(cart);
expect(order.total).toBe(46.2);
expect(wallet.balance).toBe(53.8);