Deep Stubs in Mockito: Simplifying Nested Mocking

Illustration for Deep Stubs in Mockito: Simplifying Nested Mocking
By Last updated:

When working with complex objects in enterprise systems, dependencies often have nested structures — services calling repositories, which in turn return entities with additional relationships. Writing unit tests for such scenarios can quickly become verbose.

This is where Mockito deep stubs come to the rescue. They allow you to mock chained method calls without manually creating multiple mocks.


What Are Deep Stubs?

A deep stub in Mockito lets you call methods on a mock’s return value without explicitly mocking intermediate objects.

Without Deep Stubs

CustomerRepository repository = mock(CustomerRepository.class);
Customer customer = mock(Customer.class);
Address address = mock(Address.class);

when(repository.findById(1)).thenReturn(customer);
when(customer.getAddress()).thenReturn(address);
when(address.getCity()).thenReturn("London");

assertEquals("London", repository.findById(1).getAddress().getCity());

With Deep Stubs

CustomerRepository repository = mock(CustomerRepository.class, RETURNS_DEEP_STUBS);

when(repository.findById(1).getAddress().getCity()).thenReturn("London");

assertEquals("London", repository.findById(1).getAddress().getCity());

Much cleaner — no need to mock each level separately.


Why Use Deep Stubs?

  • Reduce Boilerplate: Avoid manually creating multiple mocks.
  • Readable Tests: Focus on behavior, not mock setup.
  • Faster Prototyping: Useful for legacy systems with deep object graphs.

Analogy: Think of deep stubs as shortcuts on a map — they get you to the destination faster, but you should still know the long route.


Example: Service with Nested Dependencies

class OrderService {
    private final OrderRepository repository;

    OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    String getCustomerCity(int orderId) {
        return repository.findById(orderId)
                         .getCustomer()
                         .getAddress()
                         .getCity();
    }
}

Test with Deep Stubs

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private OrderRepository repository;

    @InjectMocks
    private OrderService service;

    @Test
    void shouldReturnCustomerCity() {
        when(repository.findById(1).getCustomer().getAddress().getCity())
                .thenReturn("Paris");

        assertEquals("Paris", service.getCustomerCity(1));
    }
}

Best Practices for Deep Stubs

  1. Use Sparingly — Overusing deep stubs can hide poor design.
  2. Prefer Refactoring — If objects are deeply nested, consider redesigning.
  3. Limit Chain Length — Long chains in tests are harder to maintain.
  4. Combine with Argument Matchers — For more flexible stubbing.
  5. Avoid in Critical Business Logic — Use them only for infrastructure-heavy code.

Pitfalls of Deep Stubs

  • Encourages Fragile Tests: Tight coupling to implementation details.
  • Hides Complexity: May mask the need to refactor.
  • Less Explicit: Harder for newcomers to see dependency structure.

If you find yourself chaining more than 3 levels, revisit your design.


Mockito + Testcontainers Example

Use Mockito for unit tests with deep stubs, but validate real-world behavior with Testcontainers:

@Test
void shouldStartDatabase() {
    try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
        postgres.start();
        assertTrue(postgres.isRunning());
    }
}

This ensures both mocked dependencies and real services are covered.


Version Tracker

  • JUnit 4 → JUnit 5: Deep stubs supported with @Mock(answer = RETURNS_DEEP_STUBS).
  • Mockito Enhancements: Improved support for chained mocks.
  • Testcontainers Ecosystem: Growing adoption for realistic integration tests.

Conclusion & Key Takeaways

Mockito deep stubs are a powerful shortcut for mocking nested method calls, but they should be used carefully.

Key Takeaways:

  • Deep stubs simplify nested mocking.
  • They reduce boilerplate but may encourage poor design.
  • Use sparingly and prefer refactoring when possible.
  • Combine with Testcontainers for realistic validation.

FAQ

1. What is a deep stub in Mockito?
A mock that allows chained method calls without explicitly mocking intermediate objects.

2. How do I enable deep stubs?
Pass RETURNS_DEEP_STUBS to mock() or use @Mock(answer = RETURNS_DEEP_STUBS).

3. Are deep stubs bad practice?
Not inherently, but overuse may indicate poor design.

4. Can I combine deep stubs with @InjectMocks?
Yes, you can inject deep-stubbed mocks into your class under test.

5. What’s the alternative to deep stubs?
Manually mock intermediate objects or refactor your design.

6. Do deep stubs impact performance?
Negligibly — but they may increase test fragility.

7. Can I verify calls on deep stubs?
Yes, you can still use verify() on chained calls.

8. Should I use deep stubs in microservices?
Use them sparingly; prefer contract tests and Testcontainers.

9. Are deep stubs supported in older Mockito versions?
Yes, since Mockito 1.8.

10. How do I avoid brittle tests with deep stubs?
Limit chain length, and focus on testing behavior, not structure.