When stubbing a Mockito spy, why can when(spy.loadData()).thenReturn(...) be dangerous, and what should you write instead?
answer
- when(...) evaluates its argument first
- On a spy that argument is the real method
- doReturn().when(spy).method() never invokes it
- doNothing() is the only void stubbing route
- doReturn takes Object — no compile-time type check
basics
~20 sBecause when(spy.loadData()) has to evaluate spy.loadData() to record the stub, and on a spy that runs the real method — with its side effects, cost, or exceptions. Use doReturn(value).when(spy).loadData(), which registers the stub without ever invoking the real method.
solid answer
~50 s`when(...)` takes a **value**, so Java must evaluate its argument first. On a mock that is harmless: the call returns a default and Mockito records it. On a **spy**, evaluating `spy.loadData()` executes the real implementation — opening a file, hitting a database, mutating state, or throwing. The classic demonstration: ```java List<String> spied = spy(new ArrayList<String>()); when(spied.get(0)).thenReturn("a"); // IndexOutOfBoundsException from the real get() doReturn("a").when(spied).get(0); // fine ``` The `do*` family — `doReturn`, `doThrow`, `doAnswer`, `doNothing`, `doCallRealMethod` — inverts the order: Mockito is put into stubbing mode *before* the method is named, so the invocation on `when(spy)` is intercepted and never reaches real code. It is also the only way to stub a `void` method, and the only safe way when the real method's side effects would corrupt the test. On spies, treat `doReturn(...).when(spy).method()` as the default form and use `when(...)` only on plain mocks.
code
java · 12 lines@Test
void stubbingASpy() {
List<String> spied = spy(new ArrayList<String>());
// BAD: real get(0) runs during setup -> IndexOutOfBoundsException
// when(spied.get(0)).thenReturn("a");
// GOOD: stub is registered without invoking the real method
doReturn("a").when(spied).get(0);
assertEquals("a", spied.get(0));
}go deeper
Recall the rule: on a spy use doReturn(...).when(spy).method(), because when(spy.method()) actually calls the method.
Explain the mechanism — argument evaluation order versus putting Mockito into stubbing mode first — and name the whole do* family including doNothing for void.
Add the second-order damage: side effects and I/O during setup, and setup calls polluting verification counts; enforce the form in review rather than case by case.
Treat it as a suite-level convention with a rationale: a rule that depends on whether a method is currently side-effect-free is a rule that breaks when someone edits that method.
## Why the two forms differ at all Mockito's stubbing API is built on a trick: you call the method you want to stub, and Mockito — which intercepted that call — remembers it as "the invocation being stubbed". The two syntaxes differ in *when* Mockito is told that a stubbing is in progress. `when(spy.loadData()).thenReturn(x)` is ordinary Java. The argument to `when` must be evaluated before `when` is entered, so `spy.loadData()` is invoked first, and only afterwards does Mockito see the recorded invocation and attach the stub. `doReturn(x).when(spy).loadData()` reverses this. `doReturn(x)` puts Mockito into stubbing mode and returns a `Stubber`. `stubber.when(spy)` returns an instrumented view of the spy that is already in "I am being stubbed" state. The subsequent `.loadData()` is intercepted and recorded — the real body never runs. ## Why it only bites on spies On a plain mock, the pre-evaluation is harmless: the real body does not exist as far as the mock is concerned, so `mock.loadData()` returns `null` and no damage is done. That is why the readable `when(...).thenReturn(...)` form is the idiomatic choice for mocks and why it is what everyone learns first. On a spy, the default answer is *call the real method*. Every consequence of that method now happens during test setup: - **Exceptions.** `spy(new ArrayList<String>())` then `when(spied.get(0))` throws `IndexOutOfBoundsException` before Mockito ever attaches the stub. The test fails in `@BeforeEach` with an error that looks unrelated to stubbing. - **Side effects.** A method that writes a row, sends a message, increments a counter, or mutates a field does all of that once, silently, during setup. Later assertions then see state nobody intended. - **Cost and I/O.** Network calls, file reads, and sleeps execute for real — the very things you were stubbing to avoid. - **Corrupted verification.** The setup call is recorded as an invocation. A later `verify(spy).loadData()` may pass because of the *setup* call rather than the code under test, and `verify(spy, times(1))` may fail with a count one higher than expected. Mockito does reset the stubbed invocation in the common case, but relying on that is not something to build a suite on. ## The full do* family - `doReturn(value).when(spy).method()` — the general replacement for `when(...).thenReturn(...)`. Note it is not type-checked at compile time: `doReturn` takes `Object`, so a wrong type fails at runtime instead. That is the one real cost of the form. - `doThrow(new IllegalStateException()).when(spy).method()` — stubbing an exception. - `doNothing().when(spy).voidMethod()` — the **only** way to neutralise a `void` method, on spies and mocks alike, because a void call cannot be passed to `when()`. - `doAnswer(invocation -> ...).when(spy).method()` — computed responses, access to arguments. - `doCallRealMethod().when(spy).method()` — the inverse: force real behaviour on a mock, or restore it on a spy. ## Consecutive stubbing Both forms chain. `doReturn(1).doReturn(2).when(spy).next()` yields 1 then 2, and `doThrow(...).doReturn(...)` mixes outcomes across successive calls — the same semantics as `thenReturn(1).thenReturn(2)`. ## What about void methods on mocks? The `when(...)` form cannot express them at all: `when(mock.doStuff())` does not compile when `doStuff` returns `void`. So `doNothing()`, `doThrow()` and `doAnswer()` are the entire vocabulary for void stubbing regardless of whether the double is a mock or a spy. That is the second reason every Mockito user eventually learns the `do*` family. ## The rule to carry **On a spy, always stub with `do*().when(spy).method()`.** Do not make it conditional on whether you believe the real method is safe — that belief is exactly what breaks when someone later adds a side effect to it. Reviewers should treat `when(spy.anything())` in a test as a defect regardless of whether it currently passes. On mocks, keep `when(...).thenReturn(...)`: it is more readable and type-checked. The asymmetry is deliberate — use the readable form where it is safe, the safe form where it is required.
- How do you stub a void method in Mockito, and why can't when() do it?Use doNothing(), doThrow(), or doAnswer() followed by .when(target).voidMethod(). when() cannot express it because its parameter is a value and a void call produces none, so when(mock.voidMethod()) does not compile. This applies to mocks and spies alike.
- Is there any downside to using doReturn(...).when(...) everywhere, including on plain mocks?Yes, one: doReturn takes an Object, so the compiler cannot check that the value matches the method's return type — a mismatch becomes a runtime error instead of a compile error. when(...).thenReturn(...) is generically typed and catches that at compile time, which is why it stays the preferred form on plain mocks where it is safe.
- Besides side effects, what else can go wrong when the real method runs during when() setup on a spy?The setup call is itself an invocation on the spy, so it can pollute verification — a later verify(spy).loadData() might be satisfied by the setup call rather than by the code under test, and times(n) counts can come out one too high. That makes the resulting test assert something different from what its author intended.
saying these in an interview costs you the question
- Believing when(spy.method()) does not invoke the real method
- Using when(...).thenReturn(...) on spies and calling it a style preference
- Thinking doNothing() is interchangeable with thenReturn(null) for void methods
- Assuming doReturn is type-checked at compile time
- Claiming the do* form is only needed for void methods