Intermediate · Prove

Use Mockito for Collaborations, Not State

Mock true boundaries and verify important messages without mirroring implementation steps.

Primary source: Spring Boot 3.5 Reference: Testing

35 min

Lesson 2 of 4

1d streak

The Architectural Question

Verifying every internal call locks tests to implementation details, so harmless refactors break suites while real bugs pass.

What this guide proves

  • Prefer real value objects
  • Stub only observations needed by the behavior
  • Avoid overspecified interaction scripts

Mocks vs Stubs vs Fakes: Verification of Behavior vs State

Martin Fowler's classic essay "Mocks Aren't Stubs" outlines crucial distinctions between test doubles that developers frequently misunderstand.

Double TypeRole in TestWhen to Choose It
DummyPassed to satisfy a parameter list; never actually inspected or invokedFilling required non-null constructor arguments
StubProvides predefined canned responses to calls made during the testProviding mock database read queries: when(repo.findById(...)).thenReturn(...)
SpyWraps a real object while recording method invocations and argumentsPartially overriding legacy code or verifying invocations on real adapters
MockPre-programmed with expectations that form a specification of expected callsVerifying interactions: verify(paymentGateway).charge(...)
FakeWorking lightweight implementation (e.g. in-memory map repository)Prefer over mocks for repositories and stores; enables state verification

Active Recall Checkpoint

What is the architectural difference between state verification and behavior verification?

Hint: Do you inspect what the object returned, or what methods it called on collaborators?

Show answer & explanation

State verification inspects the final return value or object attributes after execution. Behavior verification checks that specific methods were invoked on collaborators with specific parameters.

Modern Mockito: @ExtendWith(MockitoExtension.class) & Strict Stubbing

Modern Mockito enforces Strict Stubbing by default. In legacy setups, developers accumulated unused stubs (when(...)) that no longer matched the production code, masking test rot and misleading future maintainers.

OrderServiceTest.java
package com.example.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentClient paymentClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    void processPayment_Success() {
        Order order = new Order("ord_123", 5000);
        when(orderRepository.findById("ord_123")).thenReturn(Optional.of(order));
        when(paymentClient.charge(5000)).thenReturn(new PaymentReceipt("rcpt_999", PaymentStatus.SUCCESS));

        orderService.processOrder("ord_123");

        verify(orderRepository).save(order);
        assertThat(order.isPaid()).isTrue();
    }
}

UnnecessaryStubbingException

If you declare a when(...) stub that is never executed by the code under test, Mockito aborts the test with UnnecessaryStubbingException. This keeps test files clean, lean, and strictly synchronized with production behavior.

Active Recall Checkpoint

What does UnnecessaryStubbingException signal to a developer when refactoring a service?

Hint: Did the production code stop calling that collaborator?

Show answer & explanation

It signals that the code under test no longer calls the stubbed method. The developer should either update the test to reflect the new architecture or remove the dead stub.

Capturing Arguments & Verifying Collaboration Contracts

When collaborators perform side effects (such as publishing events or invoking third-party payment APIs), tests must verify that the payload passed to the collaborator satisfies domain contracts. Use ArgumentCaptor.

EventPublishingTest.java
@Captor
private ArgumentCaptor<OrderCreatedEvent> eventCaptor;

@Test
void placesOrder_PublishesCorrectEvent() {
    orderService.createOrder("cust_42", List.of("SKU-1"));

    verify(eventPublisher, times(1)).publishEvent(eventCaptor.capture());

    OrderCreatedEvent capturedEvent = eventCaptor.getValue();
    assertThat(capturedEvent.customerId()).isEqualTo("cust_42");
    assertThat(capturedEvent.items()).containsExactly("SKU-1");
    assertThat(capturedEvent.timestamp()).isNotNull();

    verifyNoMoreInteractions(eventPublisher);
}

Active Recall Checkpoint

Why is ArgumentCaptor preferred over writing complex ArgumentMatcher lambdas in verify() statements?

Hint: Which approach gives clearer AssertJ failure reports if an inner field is wrong?

Show answer & explanation

ArgumentCaptor allows you to extract the captured object and use standard AssertJ assertions on individual fields. If an assertion fails, AssertJ prints the exact field discrepancy, whereas custom matchers only report a generic verification failure.

Deliberate Failure Lab: The Fragile Over-Mocking Trap

Over-mocking occurs when developers mock data classes, value objects, records, and internal helper methods instead of external architectural boundaries. This creates fragile tests that mirror source code line-by-line rather than testing observable contracts.

Signs of Over-Mocking Anti-Patterns

1. Mocking a Java Record, DTO, or Entity (mock(User.class)). 2. Verifying getters: verify(user).getEmail(). 3. Mocking a collection or stream. 4. Having 15+ when(...) lines in a 20-line test. When production code changes a private helper, 40 tests fail even though the business behavior did not break.

Active Recall Checkpoint

What types of objects should NEVER be mocked in a unit test?

Hint: Should immutable data records or standard collections ever be mocked?

Show answer & explanation

Value objects, records, entities, DTOs, and standard library collections should never be mocked. Construct real instances with real test data.

Working Implementation

Code Walkthrough

Test that a placed order requests payment once and persists the resulting state.

OrderCollaborationMockitoLab.java
package com.example.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class OrderCollaborationMockitoLab {
    public interface OrderRepository { Optional<Order> findById(String id); void save(Order order); }
    public interface PaymentGateway { boolean charge(String customerId, long cents); }
    public record Order(String id, String customerId, long amount, boolean paid) {
        Order markPaid() { return new Order(id, customerId, amount, true); }
    }

    @Mock private OrderRepository orderRepository;
    @Mock private PaymentGateway paymentGateway;
    @InjectMocks private OrderCheckoutService checkoutService;
    @Captor private ArgumentCaptor<Order> orderCaptor;

    @Test
    void successfulCheckout_ChargesGatewayAndPersistsPaidOrder() {
        Order pending = new Order("ord_1", "cust_99", 5000, false);
        when(orderRepository.findById("ord_1")).thenReturn(Optional.of(pending));
        when(paymentGateway.charge("cust_99", 5000)).thenReturn(true);

        checkoutService.checkout("ord_1");

        verify(paymentGateway, times(1)).charge("cust_99", 5000);
        verify(orderRepository).save(orderCaptor.capture());
        assertThat(orderCaptor.getValue().paid()).isTrue();
        verifyNoMoreInteractions(paymentGateway);
    }
}
@ExtendWith(MockitoExtension.class) enables strict stubbing, detecting unused or obsolete stubs.
Mock only external collaborators (Repository, Gateway); keep domain records and value objects real.
ArgumentCaptor captures the saved object to verify state transitions with clear AssertJ assertions.

Synthesis & Production Verification

Production Acceptance Criteria

  • Stub gateway responses without asserting internal call patterns
  • Verify payment was requested exactly once
  • Prefer real value objects over chains of mocks

Architectural & Technical Review

Core Interview Questions

Q01What is the distinction between a mock, a stub, and a fake in test doubles?Show answer

A stub returns canned data to indirect inputs; a mock expects specific interactions and verifies behavior; a fake is a lightweight working implementation (e.g. in-memory repository).

Q02How does Mockito Strict Stubbing improve test suite health?Show answer

Strict stubbing detects unused stubs and mismatched arguments at runtime, throwing UnnecessaryStubbingException to prevent test rot and dead mock definitions.

Q03Why is ArgumentCaptor preferred over complex custom matchers in verification?Show answer

ArgumentCaptor captures the passed parameter for inspection using standard AssertJ assertions, providing clear property-level failure diffs when assertions fail.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Over-mocking domain entities, records, and value objects — Mocking data carriers creates brittle tests that pass when real objects would fail; construct real instances instead.
  • Pitfall: Accumulating unused stubs that mask behavioral drift and test rot — Strict stubbing ensures that every declared mock interaction is actively executed by the production code.

Primary Documentation & Specifications

Supplementary Module Resources