skip to content

You inherit a Mockito test suite where most tests spy on the class under test and stub out a few of its own methods. What risks does that carry, and when is stubbing a real object's own method still the right call?

level: principalimportance: should knowfreq 36%

answer

  1. Stubbed self-method = untested behaviour, still green
  2. Stub drift with no contract to anchor it
  3. Test asserts the internal call graph, not behaviour
  4. private never stubbable; final was silently ignored pre-Mockito 5
  5. Symptom of a hidden dependency — extract a collaborator

basics

~20 s

Every stubbed method is behaviour no longer under test, and the stub encodes the class's internal call graph — so refactoring breaks tests that assert nothing, and stubs drift from the real implementation while staying green. It is still justified for legacy classes you cannot yet restructure and for genuine template-method hooks.

solid answer

~1 min

**The risks.** 1. **Coverage illusion.** Whatever you stub on the object under test is no longer tested, while the test still reads as if it covers the class. 2. **Stub drift.** The real method changes; the stub does not. The test keeps passing against behaviour that no longer exists — a false green, the worst failure mode a suite has. 3. **Coupling to internals.** Stubbing a self-call asserts the class calls itself in a particular way. Inline the method, rename it, or make it private and the test breaks with no behavioural change. 4. **Modifier traps.** `private` methods are never stubbable; on Mockito 2–4 `final` methods silently were not either. **When it is still right.** Legacy classes where an untestable dependency (a static call, file access, `Thread.sleep`, `System.currentTimeMillis`) is baked into one method and restructuring is not yet affordable — spying buys the rest of the class under test today. And template-method hierarchies where a `protected` hook is a published extension point, so stubbing it targets the contract, not an implementation detail. **The rule I would set:** partial mocking of the class under test needs a comment naming the reason; more than one stubbed self-method is a refactoring ticket, not a test.

go deeper

for a junior

Recognise that stubbing a method of the class under test means that method is no longer being tested, and that mocking a collaborator is the normal alternative.

for a middle

Explain stub drift and the coupling to internal call structure, and know that private methods cannot be stubbed at all.

for a senior

Diagnose the underlying design issue — a hidden environmental dependency — and describe the extract-a-collaborator refactoring that removes the need for the spy.

for a principal

Set policy for the suite: bound the exceptions, require justification, triage the inherited cases by drift risk, and explain why an outright ban on a legacy codebase backfires.

## What the suite is actually doing A test that spies the class under test and stubs some of its methods is running a **partial mock**: real behaviour everywhere except the methods you replaced. The appeal is obvious — one method is inconvenient (it hits disk, sleeps, reads the clock, calls a static factory), so you stub it and test the rest. The cost is less obvious, which is why suites accumulate this pattern. ## The four risks, in order of severity **1. Stub drift and false greens.** This is the serious one. Suppose `computeFee()` is stubbed to return `10` because computing it is awkward. Six months later `computeFee()` changes its rounding, or starts throwing on negative input. The stub still returns `10`, the test still passes, and the suite reports that the class works — while production behaves differently. Unlike a mocked *collaborator*, where the contract is a published interface you can reason about, a stubbed *self-method* is a private implementation detail with no contract to anchor it. Nothing pushes the stub back into agreement with reality. **2. Coverage illusion.** Line coverage tools count the stubbed method as covered if any other test touches it, and the test's name still claims to test the class. Reviewers reading `orderServiceTest.placesOrder()` reasonably assume the whole path was exercised. Auditing what a suite really covers becomes a manual exercise. **3. Coupling to the internal call graph.** Stubbing `enabled()` to steer `run()` is an assertion that `run()` calls `this.enabled()`. That is not behaviour; it is structure. Inlining `enabled()`, renaming it, changing it to take a parameter, or reducing it to `private` all break the test while the class's observable behaviour is unchanged. In a codebase that refactors regularly, this converts every restructuring into test archaeology, and the usual response — reshaping the code to keep the tests happy — is precisely backwards. **4. Mechanism traps.** `private` methods cannot be stubbed at all, so the pattern pushes people to widen visibility "for testing", weakening encapsulation. On Mockito 2–4's default subclass mock maker, `final` methods were silently not stubbed — the stub was accepted and ignored, and the real method ran — producing failures that look impossible until you check the modifier. Mockito 5's inline mock maker handles final, but the private case never goes away. ## What the pattern is telling you The pull toward partial mocking almost always signals **two responsibilities in one class**: the logic you want to test, and an environmental dependency hard-wired into a method. The structural fix is to make that dependency explicit — inject a `Clock` instead of calling `System.currentTimeMillis()`, an `IdGenerator` instead of `UUID.randomUUID()`, a `ConfigLoader` instead of reading the file inline, a `Sleeper` instead of `Thread.sleep`. Then the class is testable with plain mocks, the test no longer knows the internal call graph, and the design is better independently of testing. That reframing is the answer an interviewer is listening for: the spy is not the problem, it is the symptom. ## When it is genuinely the right call **Legacy under a deadline.** You need a class under test *today* and cannot restructure it safely without tests — the classic chicken-and-egg. Spying to stub the one untestable method is a legitimate seam that gets you a safety net, after which you refactor and delete the spy. Write that intent down in the test, because otherwise the temporary becomes permanent. **Template-method hooks.** In a framework base class, a `protected` hook is a *published extension point* — subclasses are supposed to override it. Stubbing it in a test targets a real contract, not an accident of implementation, so the coupling objection largely evaporates. **Expensive but behaviour-irrelevant setup.** A method that warms a large cache or builds a heavy fixture, whose result the test does not care about, can reasonably be neutralised — provided the test is not about that behaviour. ## How I would drive the inherited suite 1. **Inventory.** Count tests that spy the class under test and, for each, the number of stubbed self-methods. One is a smell; two or more is nearly always a class doing too much. 2. **Triage by risk.** Prioritise stubs on methods that carry business rules — those are the drift candidates that produce false greens. Stubs on `sleep`-like methods are comparatively harmless. 3. **Refactor at the source.** Extract the awkward dependency into a collaborator, inject it, replace the partial mock with a plain mock. Do this as you touch the code, not as a big-bang migration. 4. **Set the rule going forward.** New partial mocks of the class under test require a comment naming the reason and a ticket if it is legacy. Never stub the method the test is actually about. 5. **Do not ban it outright.** A blanket prohibition on a codebase with real legacy pushes people toward worse workarounds — widened visibility, test-only constructors, mutable statics. Bound it instead. ## The one-sentence version A partial mock of the class under test trades away part of what the test claims to verify and buys coupling to internal structure in return; sometimes that trade is worth making, but it should always be a deliberate, documented decision rather than the default.

  • How would you refactor a class whose tests must spy on it because one method calls System.currentTimeMillis()?
    Introduce a java.time.Clock as a constructor dependency and replace the direct call with clock.instant(). Production passes Clock.systemUTC(); the test passes Clock.fixed(...). The spy disappears, the test no longer depends on the class's internal call structure, and time becomes an explicit part of the class's contract rather than a hidden one.
  • Someone argues that stubbing a self-method is no different from mocking a collaborator. What is the distinction?
    A mocked collaborator stands behind a published interface — a contract both sides agree on, which other tests can verify independently. A stubbed self-method is a private implementation detail with no contract, so nothing keeps the stub honest as the real method evolves, and the stub silently encodes how the class calls itself. That is why one is routine and the other needs justification.
  • Would coverage tooling reveal this problem?
    Not reliably. Line and branch coverage measure execution, not meaningfulness, and a stubbed method typically still shows as covered because some other test executes it. Detecting the issue takes a targeted search for spy usage on classes under test, or mutation testing, which surfaces stubbed-out logic as surviving mutants.

saying these in an interview costs you the question

  • Treating partial mocking of the class under test as normal practice rather than a bounded exception
  • Widening a method's visibility from private to package-private purely so it can be stubbed
  • Claiming the risk is only performance or readability, missing the false-green drift argument
  • Proposing a blanket ban with no path for genuine legacy code
  • Stubbing the very method the test's name says it verifies

context