Mockito Spy vs Mock — Partial Mocking

3 minute read

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

  1. @Mock = all fake. @Spy = real object with selective overrides.
  2. Always use doReturn().when(spy) — never when(spy.method()).
  3. Use spies for legacy code where you can’t inject mocks easily.
  4. Needing a spy often signals a design problem — consider refactoring.
  5. 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.

Updated: