When to Use a Spy
Fast recall
Quick revision
Core answer
doReturn(...).when(spy)... avoids calling a risky real method during setup; ordinary when(...) can still be used when that setup call is safe.Interview-ready guide
Interview answer
I use a spy when I intentionally need most methods of a real object to run, but I want to replace or verify one small part.
A mock is different because unstubbed methods return Mockito defaults.
For example, a legacy formatter may have useful real parsing logic but one overridable lookup that reaches a slow external source; a spy can keep the parser and stub that lookup. I first check that the remaining real methods have no unsafe side effects. read()` does not.
doReturn is needed only when that setup call is unsafe. If a test needs many partial stubs, I would usually extract the external behavior into a separate dependency and mock that dependency instead.
When to Use a Spy
Direct Answer
Use a spy when you deliberately want a real object's methods to run by default while stubbing or verifying a small, specific part of its behavior. A mock starts with Mockito defaults instead. A spy is useful around legacy code or for observing a real collaborator, but real methods can have side effects. doReturn(...).when(spy)... avoids calling a risky real method during setup; ordinary when(...) can still be used when that setup call is safe.
Code Example
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
private PaymentGateway mockGateway; // All methods return null/defaults
@Spy
private PaymentValidator spyValidator = new PaymentValidator(); // Real object
@Test
void testMockBehavior() {
when(mockGateway.charge(100)).thenReturn("SUCCESS");
var result = mockGateway.charge(100); // Returns "SUCCESS"
var other = mockGateway.refund(50); // Returns null (no stub)
}
@Test
void testSpyBehavior() {
// Real validateAmount() runs unless we stub it
var realResult = spyValidator.validateAmount(100); // Runs actual code
doReturn(false).when(spyValidator).validateAmount(0); // Override one method
var stubbedResult = spyValidator.validateAmount(0); // Returns false
verify(spyValidator, times(2)).validateAmount(anyInt());
}
}
Common Mistakes & Pitfalls
- Forgetting real behavior: every unstubbed spy method can execute production logic and side effects.
- Eager setup call:
when(spy.read()).thenReturn(value)callsread()while configuring the stub; usedoReturnwhen that is unsafe. - Treating
doReturnas mandatory: it is an escape hatch for risky setup calls, not a rule for every spy. - Assuming
@Spyalways needs an initializer: Mockito can construct a suitable type, but explicit construction is clearer when the object needs chosen state or constructor arguments. - Hiding a design problem: heavy partial stubbing may mean a responsibility should be moved behind an injectable collaborator.
Check your understanding
Answer these without looking back at the article.
- What is the risk of over-using Spies in your test suite?
- How do you create a Spy with @Spy annotation versus Mockito.spy()?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.