Mockito Spy vs Mock — Partial Mocking
A mock replaces everything — no real logic executes. A spy wraps a real object — real methods run unless you override them. Knowing when to use each is crucial.
Mock vs Spy
@Mock @Spy
────────────────────────────── ──────────────────────────────
All methods return defaults All methods call REAL code
No real logic executes Real logic executes
You stub what you NEED You stub what you want to OVERRIDE
@Mock
private UserRepository mockRepo;
// mockRepo.findAll() → returns empty list (default)
// mockRepo.count() → returns 0 (default)
@Spy
private UserRepository spyRepo = new InMemoryUserRepository();
// spyRepo.findAll() → calls REAL findAll()
// spyRepo.count() → calls REAL count()
Basic Spy Example
@ExtendWith(MockitoExtension.class)
class SpyTest {
@Spy
private List<String> spyList = new ArrayList<>();
@Test
void spyCallsRealMethods() {
spyList.add("one");
spyList.add("two");
assertEquals(2, spyList.size()); // real size()
assertEquals("one", spyList.get(0)); // real get()
// Override just one method
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size()); // stubbed
assertEquals("one", spyList.get(0)); // still real
}
}
⚠️ Critical: doReturn vs when with Spies
@Spy
private ExpensiveService spyService = new ExpensiveService();
// ❌ WRONG — calls the REAL method during stubbing!
when(spyService.heavyComputation()).thenReturn("cached");
// ✅ CORRECT — does NOT call the real method
doReturn("cached").when(spyService).heavyComputation();
With spies, always use
doReturn().when(spy)to avoid side effects during stubbing.
When to Use Spy
1. Legacy Code Without Interfaces
// Can't easily mock — no interface, complex constructor
public class LegacyPaymentProcessor {
public boolean charge(String cardNumber, BigDecimal amount) {
// complex legacy logic
return true;
}
public String generateReceipt(String transactionId) {
// calls external system
return "receipt-" + transactionId;
}
}
@Spy
private LegacyPaymentProcessor processor = new LegacyPaymentProcessor();
@Test
void testWithSpy() {
// Override only the external call
doReturn("mock-receipt").when(processor).generateReceipt(any());
// charge() still runs real logic
boolean charged = processor.charge("4111111111111111", new BigDecimal("99.99"));
assertTrue(charged);
}
2. Testing a Method That Calls Another Method in the Same Class
public class ReportService {
public String generateReport(Long userId) {
List<Order> orders = fetchOrders(userId); // want to mock this
return formatReport(orders); // want to test this
}
protected List<Order> fetchOrders(Long userId) {
// calls database — want to mock in tests
return orderRepository.findByUserId(userId);
}
private String formatReport(List<Order> orders) {
// formatting logic — want to test this
return orders.stream()
.map(o -> o.getId() + ": $" + o.getTotal())
.collect(Collectors.joining("\n"));
}
}
@Spy
private ReportService reportService = new ReportService();
@Test
void shouldFormatReport() {
List<Order> testOrders = List.of(
new Order("ORD-1", new BigDecimal("50.00")),
new Order("ORD-2", new BigDecimal("75.00"))
);
doReturn(testOrders).when(reportService).fetchOrders(anyLong());
String report = reportService.generateReport(1L);
assertTrue(report.contains("ORD-1: $50.00"));
assertTrue(report.contains("ORD-2: $75.00"));
}
Comparison Table
| Feature | @Mock |
@Spy |
|---|---|---|
| Real methods execute | ❌ No | ✅ Yes |
| Default return values | Null/0/false/empty | Real return values |
| Needs a real instance | No | Yes |
| Stub syntax | when().thenReturn() |
doReturn().when() |
| Use case | Isolate dependencies | Partial override |
| Risk of misuse | Low | High |
Dangers of Overusing Spies
⚠️ If you need a spy, your design might need improvement.
Spy needed? → Consider:
1. Extract the behavior into a separate class
2. Use dependency injection
3. Introduce an interface
Spy = code smell detector
| Smell | Better Design |
|---|---|
| Spying on same class under test | Extract method into a dependency |
| Spying to mock private methods | Refactor — make it a separate class |
| Spy needed for every test | Your class has too many responsibilities |
Spy with @InjectMocks
@Mock private UserRepository userRepo;
@Spy private AuditLogger auditLogger = new AuditLogger();
@InjectMocks private UserService userService;
// userService uses real AuditLogger (spy) and mock UserRepository
Key Takeaways
@Mock= all fake.@Spy= real object with selective overrides.- Always use
doReturn().when(spy)— neverwhen(spy.method()). - Use spies for legacy code where you can’t inject mocks easily.
- Needing a spy often signals a design problem — consider refactoring.
- Prefer
@Mock— it’s simpler, safer, and more explicit.
What’s Next?
Next: Test-Driven Development (TDD) — write tests before code with the Red-Green-Refactor cycle.