Why must you usually stub a Mockito spy with doReturn().when() instead of when().thenReturn()?
answer
- Args evaluated before when() runs
- Spy: when(spy.foo()) really calls foo()
- doReturn().when(spy).foo() never calls foo()
- Same for void: doNothing/doThrow
- Silent side-effect risk, not just exceptions
basics
~10 sBecause when(spy.foo()).thenReturn(x) actually runs the real foo() first (Java evaluates the argument before when sees it). If foo() has side effects or throws, your stub line breaks. doReturn(x).when(spy).foo() never calls the real method.
solid answer
~40 sThe two stubbing styles differ in evaluation order. In when(spy.foo()).thenReturn(x), Java must evaluate the argument spy.foo() before invoking when(...). On a spy, foo() is a real method, so it actually executes — and if it throws (e.g. get(0) on an empty list) or has side effects (writes to a DB, mutates state), the stubbing line itself fails or causes damage. The doReturn(x).when(spy).foo() form sidesteps this: when(spy) returns a stubbing-mode proxy, and the subsequent .foo() call is intercepted, not executed, so the real method never runs. The same logic applies to doNothing(), doThrow(), and doAnswer() for void or risky methods. On a plain mock both styles are safe because there is no real method to run — but using doReturn().when() consistently on spies (and on any method with side effects) is the defensive idiom.
code
java · 14 linesList<String> spy = Mockito.spy(new ArrayList<>());
// UNSAFE on a spy: get(0) actually runs -> IndexOutOfBoundsException now,
// before the stub is ever set.
// when(spy.get(0)).thenReturn("x"); // throws here
// SAFE: real get(0) is never invoked during stubbing
doReturn("x").when(spy).get(0);
assertEquals("x", spy.get(0));
// void method: only the do* form can express this
doNothing().when(spy).clear();
spy.clear(); // real clear() suppressed
verify(spy).clear();go deeper
May just memorize 'use doReturn().when() for spies' as a rule without the reason.
Knows the rule and that the real method runs with when(spy.foo()), and uses do* for void methods.
Explains the argument-evaluation-order mechanism precisely, notes the silent-side-effect (not just exception) risk, and knows do* is also required for void stubbing.
Connects it to strict-stubbing bookkeeping, sets a team convention (do*().when() for spies), and reasons about why heavy spy stubbing indicates designs that resist clean isolation.
## The core mechanic: argument evaluation order In Java, before a method is called, **all of its arguments are fully evaluated**. Consider: ```java when(spy.foo()).thenReturn(42); ``` This is really `when( spy.foo() )` — so the JVM **calls `spy.foo()` first**, gets its return value, and passes that to `when(...)`. Mockito's `when(...)` works by inspecting the *last invocation it recorded* on a double, which is why this normally works for mocks: calling `mock.foo()` on a mock doesn't run real code, it just registers "foo was called" for `when` to pick up. On a **spy**, however, `spy.foo()` is a **real method invocation** — the actual implementation runs. Two bad things can follow: 1. **It throws.** Classic case: `when(spy.get(0)).thenReturn("x")` on a spy of an empty `ArrayList` runs the real `get(0)`, which throws `IndexOutOfBoundsException` — your stubbing line crashes before any stub is set. 2. **It has side effects.** If `foo()` writes to a database, sends a message, increments a counter, or mutates fields, that happens for real during what you thought was just *setup*. ## The fix: the do*().when() family ```java doReturn(42).when(spy).foo(); ``` Read it as: "set up the answer 42, **and the method it applies to is** `spy.foo()`." Here `when(spy)` returns a **stubbing proxy**; the following `.foo()` is **intercepted by that proxy and not executed**. So the real `foo()` is never called during stubbing. The whole family behaves the same way: - `doReturn(x).when(spy).foo()` — canned return - `doNothing().when(spy).voidMethod()` — suppress a real void method - `doThrow(ex).when(spy).foo()` — make it throw - `doAnswer(inv -> ...).when(spy).foo()` — dynamic answer ## When `when().thenReturn()` is still fine - On a **mock**, always — there is no real method to trigger. - On a **spy**, only if `foo()` is **safe** (pure, no side effects, won't throw with current state). Many teams still standardize on `do*().when()` for spies to avoid having to reason about safety per call. - `do*` also has a separate use: stubbing **void methods**, which `when(...).thenReturn(...)` can't express at all (a void call can't be the argument of `when`). That's why `doThrow`/`doNothing` exist even on mocks. ## Subtle trap: it can pass silently The danger isn't always a loud exception. If `spy.foo()` returns normally but **mutates state** or **performs I/O**, the test may still pass while having done real work during setup — a silent, hard-to-trace bug. That's the deeper reason seniors prefer the `do*` form on spies: it removes a class of latent setup-time side effects entirely. ## Strict stubbing interaction With Mockito's default **strict stubs** (JUnit 5 `MockitoExtension`), a real call made during `when(spy.foo())` setup can also produce confusing `PotentialStubbingProblem`/unnecessary-stubbing reports, because Mockito's invocation accounting gets muddied by the real call. The `do*().when()` form keeps the recorded-invocation bookkeeping clean.
- Is when().thenReturn() ever wrong on a plain mock for the same reason?No — on a mock the method has no real implementation, so calling mock.foo() during when(...) doesn't execute real logic. The hazard is specific to spies (and to expressing void-method stubs, which when().thenReturn() can't do).
- Beyond exceptions, what makes the when(spy.foo()) form dangerous even when it 'works'?If foo() has side effects (I/O, state mutation), they happen for real during setup, so the test can pass while silently performing real work — a latent bug the do*().when() form prevents.
saying these in an interview costs you the question
- Saying the difference is just style/readability — it changes whether the real method executes.
- Claiming doReturn().when() also calls the real method — it specifically does not.
- Thinking the only risk is an exception, ignoring silent side effects during setup.
- Believing void methods can be stubbed with when().thenReturn() — they require doNothing()/doThrow().