skip to content

When should you mock a collaborator versus using the real object (or a fake), and what are the risks of over-mocking?

level: seniorimportance: should knowfreq 55%

answer

  1. mock slow/external/non-deterministic/boundary deps
  2. don't mock value objects or pure logic
  3. fakes beat mocks for stateful collaborators
  4. over-mocking -> brittle, implementation-coupled tests
  5. mocks hide integration bugs -> add integration tests

basics

~20 s

Mock collaborators that are slow, external, non-deterministic, or hard to set up (databases, HTTP, clocks). Use the real object for simple value or pure-logic dependencies. Over-mocking couples tests to implementation details and can hide real integration bugs.

solid answer

~50 s

Reach for a mock when the collaborator is an awkward boundary: external I/O (HTTP, database, message broker), non-deterministic (clock, random, UUID), slow, or stateful in a way that's hard to arrange. Mocking it makes the test fast, deterministic, and focused on the SUT. Conversely, don't mock simple value objects, pure functions, or cheap in-process logic — using the real thing gives more realistic coverage with less ceremony; sometimes a fake (e.g. an in-memory repository) is a better double than a mock because it behaves correctly across many calls. The main risk of over-mocking is brittle, tautological tests: when you stub and verify every interaction, the test mirrors the implementation rather than the behaviour, so any refactor breaks it even though behaviour is unchanged. Mock-heavy unit tests also leave the integration between real components untested, so you need integration tests to cover the seams the mocks hid. A good heuristic: mock at architectural boundaries (ports/adapters), prefer real objects within a cohesive unit, and verify outcomes over interactions where possible.

go deeper

for a junior

Knows mocks replace real dependencies and that you mock things like databases and external services.

for a middle

Can articulate concrete reasons to mock (slow/external/non-deterministic) and that you shouldn't mock trivial value objects.

for a senior

Weighs mock vs fake vs real, recognizes over-mocking brittleness and the integration-bug blind spot, and prefers state over interaction verification.

for a principal

Defines the team's mocking philosophy and test pyramid — boundary mocking, fakes for stateful ports, contract/integration coverage for mocked seams — and reasons about long-term test maintainability.

## The spectrum of test doubles A **test double** replaces a real collaborator in a test. The common kinds: - **Dummy** — passed but never used (just to fill a parameter). - **Stub** — returns canned answers to calls (state-based). - **Mock** — a stub that *also* records and lets you verify interactions (interaction-based). - **Spy** — wraps a real object; real methods run unless overridden. - **Fake** — a real, working, but simplified implementation (e.g. an in-memory `Map`-backed repository). Mockito's `mock(...)` gives you stub+mock capability. Choosing the right double is a design decision, not a mechanical one. ## When mocking is the right call Mock a collaborator when using the real one would make the test: 1. **Slow** — real DB/network/disk. 2. **Non-deterministic** — `Clock`, `Random`, `UUID.randomUUID()`, current time, external services that change. 3. **Hard to arrange** — requires elaborate setup, credentials, or unreachable states (e.g. forcing a `TimeoutException`). 4. **An external/architectural boundary** — a *port* in hexagonal/ports-and-adapters terms: the `PaymentGateway`, `EmailSender`, `OrderRepository` interface. These are exactly the seams a unit test wants to control. Mocking lets you assert 'the SUT *asked* the gateway to charge £10' without a real charge. Mocking also shines when you specifically want **interaction verification** — proving the SUT called a side-effecting collaborator the right way (`verify(email).send(expected)`), where there is no return value to assert on. ## When NOT to mock - **Value objects / data carriers** (`Money`, `LocalDate`, DTOs) — just construct the real thing; mocking them is pure noise and can even hide equality bugs. - **Pure functions / cheap deterministic logic** — a real call is faster to write, more realistic, and refactor-safe. - **Code you don't own at a fine grain** — mocking a third-party type to deeply specific call sequences couples you to *their* internals; prefer wrapping it behind your own interface and mocking that. - **When a fake is better** — for a repository used across many calls in one test, an in-memory **fake** behaves consistently (put then get returns what you put), whereas a mock forces you to script each call and can become a fragile pile of `when(...)`. ## The cost of over-mocking 1. **Brittleness / tautology.** If a test stubs and `verify`s every interaction, it encodes *how* the SUT works, not *what* it produces. Refactoring the SUT (same behaviour, different call sequence) breaks the test. Such tests have low value: they change whenever the code changes, by construction. 2. **False confidence / hidden integration bugs.** Mocks replace real behaviour with your *assumption* of it. If the real collaborator behaves differently (different return shape, an exception you didn't model), the mock-based test still passes while production breaks. Mocks test the SUT against a *belief* about its dependencies; only integration tests check that belief. 3. **Readability collapse.** A test with a dozen `when(...)/verify(...)` lines obscures the actual scenario. ## Heuristics to balance it - **Mock at boundaries, real within the unit.** Treat the unit as a cohesive cluster; mock only what crosses an architectural seam (ports/adapters). - **Prefer state verification over interaction verification** when a return value or observable outcome exists — assert the result, don't assert the call. - **Use fakes for stateful collaborators** exercised across many calls. - **Back mock-heavy unit tests with integration tests** at the real seams so the mocked assumptions are validated somewhere. - **Avoid `RETURNS_DEEP_STUBS`** and deep stub chains — they usually signal a Law-of-Demeter violation that better design (or a fake) would remove. The senior judgement is recognizing that mocks are a *tool for isolating boundaries*, not a default reflex for every dependency.

  • Why can a fake be a better choice than a mock for a repository?
    A fake (e.g. in-memory map) behaves correctly and consistently across many calls — put then get returns what you stored — so you don't script each interaction, and the test stays robust to call-order changes; a mock would need stubbing per call and become fragile.
  • How do you guard against mocks hiding real integration bugs?
    Complement mock-heavy unit tests with integration/contract tests at the real seams (real DB via Testcontainers, real HTTP), so the assumptions baked into the mocks are validated against actual collaborator behaviour.

saying these in an interview costs you the question

  • Mocking everything reflexively, including value objects and pure functions
  • Verifying every interaction so the test mirrors the implementation
  • Believing passing mock-based unit tests prove the system integrates correctly
  • Using deep stub chains instead of fixing a Law-of-Demeter violation

context