Why does stubbing a Mockito spy with when(spy.loadAll()).thenReturn(list) still execute the real loadAll() body, and what form avoids that?
answer
- arguments evaluated before when() is called
- spy delegates unstubbed calls to the real object
- real work happens in the arrange block
- doReturn(x).when(spy).call() records without executing
- void on a spy → do-family is the only option
basics
~20 sJava evaluates spy.loadAll() before when() is called, and on a spy an un-stubbed call runs the real method — side effects, database hits, exceptions and all. Use doReturn(list).when(spy).loadAll(), which records the call without invoking it.
solid answer
~50 sArgument evaluation order is the whole story. In `when(spy.loadAll())`, the JVM must evaluate `spy.loadAll()` before it can call `when(...)`. A spy wraps a real instance and delegates any invocation that is not yet stubbed, so the real `loadAll()` runs right there in the arrange block — hitting the database, throwing, or mutating state — and only then does Mockito register the stubbing. The do-family inverts the order: ```java doReturn(list).when(spy).loadAll(); ``` Here `when(spy)` puts the spy into stubbing mode first, so the subsequent `loadAll()` is recorded, not executed. The most visible symptom is an exception thrown from the setup line — a spy whose real method throws `IllegalStateException` on an unconfigured object cannot be stubbed the `when(...)` way at all. Silent side effects are worse, because the test still passes while having performed real work. Rule I'd give a team: on a spy, always use the do-family; the `when(...)` form is for plain mocks.
code
java · 11 linesOrderRepository real = new OrderRepository(dataSource);
OrderRepository spy = Mockito.spy(real);
// BAD: loadAll() actually hits the database here, during setup
when(spy.loadAll()).thenReturn(List.of(order));
// GOOD: records the invocation without executing it
doReturn(List.of(order)).when(spy).loadAll();
// void side effect suppressed the only way possible
doNothing().when(spy).flush();go deeper
State the mechanism in one line — the argument is evaluated first, and a spy runs the real method — then show doReturn(...).when(spy).call().
Add the three symptoms (setup exception, silent side effect, inflated verification count) and note that void methods on a spy leave no alternative.
Give the team rule, explain the type-safety cost of doReturn, and be able to diagnose the failure from a stack trace pointing at the arrange line.
Treat widespread spy usage as design feedback — the class under test is mixing responsibilities — and prefer extracting the suppressed behaviour into a collaborator that a plain mock can replace.
## What a spy is `Mockito.spy(realObject)` (or `@Spy`) creates a partial mock: an object that delegates every invocation to real behaviour *unless* that invocation has been stubbed. Mocks are the opposite default — every unstubbed method returns an empty value and does nothing. That single difference in default is what makes spy stubbing syntax-sensitive. ## The evaluation-order problem `Mockito.when` is a static method with signature `when(T methodCall)`. Java evaluates arguments before invoking the method, so in ```java when(spy.loadAll()).thenReturn(list); ``` the sequence is: 1. `spy.loadAll()` is invoked. It is not stubbed yet, so the spy delegates to the **real** implementation. The real method opens a connection, reads a file, throws, mutates a field — whatever it does in production, it does now, in your test setup. 2. Its return value is passed to `when(...)`. 3. Mockito, having also recorded the invocation internally, registers the stubbing. The stubbing does end up correct; the damage is the unwanted step 1. On a plain mock this is harmless, because step 1 just returns a default value. On a spy it is real work. ## Three ways it bites **An exception in the arrange block.** If the real method throws (uninitialised state, missing connection, guard clause), the test fails on the setup line with a stack trace from production code, and the message points nowhere near the actual mistake. This is the most common way developers discover the rule. **Silent side effects.** The real method writes a row, publishes an event, increments a counter, or populates a cache. The test then passes, but it did real work and may leave state that breaks the next test or makes an interaction assertion count one extra call. **Wrong verification counts.** The setup invocation is a real invocation on the spy, so `verify(spy, times(1)).loadAll()` can fail with "wanted 1 time but was 2" because the arrange block consumed one. ## The fix ```java doReturn(list).when(spy).loadAll(); ``` `doReturn(list)` yields a `Stubber`; `stubber.when(spy)` switches the spy into stubbing mode and returns it; the following `loadAll()` is captured as the invocation to stub and is never executed. Nothing real runs. The same applies to every member of the family — `doThrow`, `doAnswer`, `doNothing`, `doCallRealMethod` — and to argument matchers, which behave exactly as usual: ```java doReturn(list).when(spy).findBy(anyString()); doNothing().when(spy).flush(); // suppress a real side effect ``` ## Void methods on spies are the sharpest case A void method on a spy cannot be stubbed with `when(...)` at all (a void call is not a legal argument), so the do-family is the only option — and it is also the only way to stop a real `flush()`, `send()` or `close()` from actually happening. `doNothing().when(spy).flush()` is the canonical suppression. ## What spies do not do A spy created from an existing instance is a **copy**: Mockito instantiates a proxy and copies the real object's fields into it. The original object is not affected by stubbing, and — importantly — internal calls made by the real code do not go through the proxy. If `loadAll()` internally calls `this.fetchPage()`, stubbing `fetchPage()` on the spy has no effect on that internal call, because the real method's `this` is the spy copy only when the outer call entered through the proxy; self-invocation inside a real method body does dispatch on the spy in Mockito's implementation, but code that captured `this` earlier, or was invoked reflectively, will not. Treat internal-call stubbing as fragile either way. ## Why spies are a smell more often than not Needing to stub part of the very class under test usually means the class is doing two jobs: the part you want to test and the part you want to suppress. Extracting the suppressed part into a collaborator lets you use a plain mock and removes the whole syntax hazard. Spies are legitimate for third-party classes you cannot restructure, for legacy code you are pinning before refactoring, and occasionally for large value-holding objects, but a suite full of spies is design feedback. ## Version note Since Mockito 5, the inline mock maker is the default, which allows spying final classes and final methods that older subclass-based mock makers could not intercept. The evaluation-order rule discussed here is unchanged across versions. ## Guidance Adopt one rule and apply it mechanically: **plain mock → `when(...).thenReturn(...)`; spy → `doReturn(...).when(spy)...`**. It costs a little type safety on the spy path and saves an entire class of hard-to-read failures.
- Does the same hazard exist when stubbing a plain mock with when(mock.call())?The evaluation order is identical, but there is no harm: an unstubbed method on a plain mock returns a default value (null, 0, false, an empty collection) and performs no work. That is why when(...).thenReturn(...) is the recommended default for mocks — it costs nothing and keeps compile-time type checking, which the doReturn form gives up.
- How do you spot this problem when a test fails with an unexpected exception?Look at which line the stack trace points to. If it is a when(spy...) setup line and the trace runs through production code, the real method executed during arrange. The same cause explains verification counts that are one higher than expected, since the setup invocation is a genuine invocation on the spy. Switching that stubbing to doReturn/doThrow/doNothing resolves both symptoms.
- Is it ever fine to leave the when(spy.method()) form in place?Only when the real method is provably free of side effects and cannot throw — a trivial getter, for instance. Even then it is not worth the ambiguity: a future change to that method turns a passing test into a mysterious one. Teams are better served by a blanket rule that spies are always stubbed with the do-family.
saying these in an interview costs you the question
- Saying Mockito 'decides' whether to call the real method inside when(), rather than Java evaluating the argument first
- Believing doReturn and when/thenReturn behave identically on a spy
- Thinking a spy leaves the original object's methods untouched, so a real call during setup is harmless
- Claiming the real invocation during setup does not count toward verification
- Using spies as the default test double and treating the syntax rule as the only issue