Mockito’s mock()
creates a dummy object where all methods are stubbed unless specified. But what if you want to call some real methods while still mocking others? That’s where spies (partial mocks) come in.
In this tutorial, we’ll explore spies in Mockito, their use cases, best practices, and real-world scenarios with JUnit 5.
Why Use Spies?
- Partial Mocking: Run real code for some methods, while mocking others.
- Legacy Code Testing: Useful when refactoring old systems where full mocking isn’t possible.
- Fine-grained Control: Mix real behavior with mock flexibility.
- Verification: Ensure certain methods are called without losing functionality.
Think of spies like understudies in a play who perform parts of the role while still standing in for the lead actor.
Creating a Spy in Mockito
Example: Calculator Service
class Calculator {
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
}
Spy Test
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void shouldSpyOnMultiplyMethod() {
Calculator realCalculator = new Calculator();
Calculator spyCalculator = spy(realCalculator);
// Stub multiply method
when(spyCalculator.multiply(2, 3)).thenReturn(100);
// add() calls real method
assertEquals(5, spyCalculator.add(2, 3));
// multiply() calls stubbed method
assertEquals(100, spyCalculator.multiply(2, 3));
verify(spyCalculator).add(2, 3);
verify(spyCalculator).multiply(2, 3);
}
}
Here:
add()
executes the real method.multiply()
is stubbed.
Difference Between Mocks and Spies
Feature | Mock (mock() ) |
Spy (spy() ) |
---|---|---|
Default Behavior | Methods return default values | Calls real methods |
Stubbing | Required for all behavior | Optional (can mix) |
Use Case | Pure unit isolation | Partial behavior testing |
Common Pitfall: Using when() with Spies
For spies, calling when(spy.method())
may invoke the real method. To avoid unintended side effects, use doReturn()
:
doReturn(200).when(spyCalculator).multiply(10, 20);
This ensures the real method is not executed during stubbing.
Real-World Example: Order Processing
class OrderService {
double calculateTotal(double price, int quantity) {
return price * quantity;
}
void placeOrder(double price, int quantity) {
double total = calculateTotal(price, quantity);
System.out.println("Order placed: $" + total);
}
}
Test with Spy
@Test
void shouldStubCalculateTotal() {
OrderService spyService = spy(new OrderService());
doReturn(500.0).when(spyService).calculateTotal(anyDouble(), anyInt());
spyService.placeOrder(100.0, 5);
verify(spyService).calculateTotal(100.0, 5);
verify(spyService).placeOrder(100.0, 5);
}
Here, calculateTotal()
is stubbed while placeOrder()
runs real logic.
Best Practices for Using Spies
- Use spies sparingly — prefer full mocks or real objects when possible.
- Use
doReturn()
for stubbing spy methods to avoid side effects. - Don’t spy on complex objects — it may lead to fragile tests.
- Ideal for testing legacy systems where code refactoring is limited.
- Combine with
verify()
to ensure interactions happened as expected.
Mockito + Testcontainers Example
Spies are great for unit tests. For integration, pair them with Testcontainers:
import org.testcontainers.containers.MySQLContainer;
@Test
void shouldStartDatabase() {
try (MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8")) {
mysql.start();
assert mysql.isRunning();
}
}
This ensures both fast unit testing with spies and real integration testing with containers.
Version Tracker
- JUnit 4 → JUnit 5: Seamless integration with
@ExtendWith(MockitoExtension.class)
. - Mockito Updates: Added inline mocking for final classes and static methods.
- Testcontainers Growth: Expanding ecosystem for hybrid testing.
Conclusion & Key Takeaways
Spies in Mockito provide the ability to partially mock objects, mixing real behavior with stubbed responses. They are powerful but should be used carefully to avoid brittle tests.
Key Takeaways:
- Use
spy()
for partial mocks. - Prefer
doReturn()
for stubbing spy methods. - Verify interactions with
verify()
. - Use spies in legacy or hybrid testing scenarios.
- Balance spies with mocks and Testcontainers for full coverage.
FAQ
1. What’s the difference between mock() and spy()?
Mocks stub everything by default, while spies call real methods unless stubbed.
2. When should I use a spy?
When you need partial real behavior but want to override specific methods.
3. Can I stub private methods with spies?
No, Mockito does not support private method mocking.
4. How do I prevent side effects when stubbing spies?
Use doReturn()
instead of when()
.
5. Do spies work with constructors?
Yes, but constructor logic is executed, so be cautious.
6. Should I spy on all dependencies?
No, spies are for special cases — mocks are safer for general use.
7. Can I spy on final classes or methods?
Yes, in newer Mockito versions with inline mocking.
8. Do spies improve test speed?
They can, but the main goal is flexibility, not speed.
9. Can I combine spies with argument matchers?
Yes, spies fully support any()
, eq()
, argThat()
, etc.
10. Should I migrate from JUnit 4 to JUnit 5 for spies?
Yes, JUnit 5 provides cleaner integration with Mockito extensions.