Mocking & Test Doubles
Definition

Mockito Basics
// CREATING MOCKS
// Annotation-based (preferred)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@InjectMocks // Injects mocks into constructor
private OrderService orderService;
// Tests...
}
// Programmatic
OrderRepository mockRepo = mock(OrderRepository.class);
PaymentService mockPayment = mock(PaymentService.class);
OrderService service = new OrderService(mockRepo, mockPayment);
// STUBBING (define behavior)
// Return value
when(repository.findById("123")).thenReturn(Optional.of(order));
// Throw exception
when(paymentService.charge(any())).thenThrow(new PaymentException());
// Multiple calls return different values
when(repository.findAll())
.thenReturn(List.of(order1))
.thenReturn(List.of(order1, order2));
// Answer based on input
when(repository.save(any(Order.class))).thenAnswer(invocation -> {
Order order = invocation.getArgument(0);
order.setId(UUID.randomUUID().toString());
return order;
});
// Void methods
doThrow(new RuntimeException()).when(emailService).send(any());
doNothing().when(logger).log(any());
Verification
// VERIFYING INTERACTIONS
// Was method called?
verify(repository).save(order);
// Called with specific arguments
verify(emailService).sendEmail(eq("[email protected]"), anyString());
// Called exact number of times
verify(repository, times(2)).findById(any());
verify(repository, never()).delete(any());
verify(repository, atLeast(1)).save(any());
verify(repository, atMost(3)).findAll();
// Called with argument matching
verify(repository).save(argThat(order ->
order.getStatus() == OrderStatus.PENDING
));
// Capture arguments for detailed assertions
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());
Order savedOrder = captor.getValue();
assertThat(savedOrder.getTotal()).isEqualTo(Money.of(100));
// Verify call order
InOrder inOrder = inOrder(paymentService, repository);
inOrder.verify(paymentService).charge(any());
inOrder.verify(repository).save(any());
// No more interactions
verifyNoMoreInteractions(repository);
// Zero interactions
verifyNoInteractions(emailService);
Spies and Partial Mocking
// SPY: Real object with partial mocking
// Create spy
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);
// Real methods are called
spyList.add("one"); // Actually adds to list
assertThat(spyList.size()).isEqualTo(1);
// But can stub specific methods
doReturn(100).when(spyList).size();
assertThat(spyList.size()).isEqualTo(100);
// ANNOTATION-BASED SPY
@Spy
private OrderValidator orderValidator = new OrderValidator();
@Test
void testWithSpy() {
// Real validation logic runs
// But can stub specific methods
doReturn(true).when(orderValidator).checkInventory(any());
}
// USE CASES FOR SPIES:
// • Testing legacy code that can't be easily mocked
// • When most behavior should be real
// • Verifying that real methods were called
// CAUTION:
// Prefer mocks over spies in most cases
// Spies can be a sign of code smell (tight coupling)
// PARTIAL MOCKING (alternative)
@Mock
private OrderService orderService;
@Test
void testPartialMock() {
// Stub one method
when(orderService.calculateTotal(any())).thenReturn(Money.of(100));
// Call real method for others
when(orderService.validate(any())).thenCallRealMethod();
}
When to Mock

Fakes
// FAKE: Working but simplified implementation
// Interface
interface UserRepository {
User save(User user);
Optional<User> findById(String id);
void delete(String id);
}
// Fake implementation
class FakeUserRepository implements UserRepository {
private final Map<String, User> users = new HashMap<>();
@Override
public User save(User user) {
if (user.getId() == null) {
user.setId(UUID.randomUUID().toString());
}
users.put(user.getId(), user);
return user;
}
@Override
public Optional<User> findById(String id) {
return Optional.ofNullable(users.get(id));
}
@Override
public void delete(String id) {
users.remove(id);
}
}
// Usage in tests
class UserServiceTest {
private final UserRepository repository = new FakeUserRepository();
private final UserService service = new UserService(repository);
@Test
void shouldCreateUser() {
User user = service.create("John", "[email protected]");
assertThat(user.getId()).isNotNull();
assertThat(repository.findById(user.getId())).isPresent();
}
}
// WHEN TO USE FAKES:
// • Complex state that's hard to stub
// • Need realistic behavior across multiple calls
// • Integration-like tests without real infrastructure
// • Shared test fixtures across many tests
Tips & Tricks
