Using Spies in Mockito: Partial Mocks Explained

Illustration for Using Spies in Mockito: Partial Mocks Explained
By Last updated:

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.