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
Lesson 2 of 4
1d streakThe 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 Type | Role in Test | When to Choose It |
|---|---|---|
| Dummy | Passed to satisfy a parameter list; never actually inspected or invoked | Filling required non-null constructor arguments |
| Stub | Provides predefined canned responses to calls made during the test | Providing mock database read queries: when(repo.findById(...)).thenReturn(...) |
| Spy | Wraps a real object while recording method invocations and arguments | Partially overriding legacy code or verifying invocations on real adapters |
| Mock | Pre-programmed with expectations that form a specification of expected calls | Verifying interactions: verify(paymentGateway).charge(...) |
| Fake | Working 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.
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
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.
@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
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.
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);
}
}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
Key Takeaway
Mockito tests external collaboration contracts and side effects with strict stubbing and argument capture, while state verification on real domain objects avoids fragile over-mocking.
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
Lesson Primary References
Supplementary Module Resources