Advanced Mockito Usage
Unit testing is a cornerstone of modern software development, and in the Java ecosystem the combination of JUnit and Mockito has become the de facto standard. Basic mocking, stubbing, and verification cover most straightforward scenarios, but real-world business logic is rarely straightforward. When dependencies interact in complex ways, you need more sophisticated testing techniques. This article walks through several advanced Mockito features—ArgumentCaptor, static method mocking, thenAnswer, and spy—and shows how to apply them to write more robust and maintainable unit tests.
Routine verification often relies on matchers such as eq() or any(). These are useful when you only care whether a method was invoked, but they fall short when you need to inspect the actual object passed to a dependency. ArgumentCaptor solves this by intercepting the argument at verification time so you can assert on its fields and internal state.
Imagine a UserService that delegates email sending to an EmailService:
public class UserService {
private final EmailService emailService;
public UserService(EmailService emailService) {
this.emailService = emailService;
}
public void registerUser(String username, String email) {
EmailMessage message = new EmailMessage(email, "Welcome", "Hello " + username);
emailService.sendEmail(message);
}
}
In a test, you can capture the EmailMessage instance and verify its recipient, subject, and body:
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@Test
void testRegistrationSendsWelcomeEmail() {
EmailService emailService = mock(EmailService.class);
UserService userService = new UserService(emailService);
userService.registerUser("Alice", "alice@example.com");
ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class);
verify(emailService).sendEmail(captor.capture());
EmailMessage message = captor.getValue();
assertEquals("alice@example.com", message.getRecipient());
assertEquals("Welcome", message.getSubject());
assertTrue(message.getBody().contains("Alice"));
}
The capture() method tells Mockito to store the argument instead of merely matching it. After verification, getValue() returns the captured object, allowing fine-grained assertions that would be awkward with standard argument matchers.
Mocking Static Methods with Mockito Inline
For years, Mockito could not mock static methods, pushing teams toward heavier frameworks such as PowerMock. Since Mockito 3.4.0, the mockito-inline extension provides built-in support for static mocking, making it possible to isolate code that calls static utilities without refactoring them into instance methods.
Consider an OrderService that validates input using a static helper:
public class OrderService {
public boolean processOrder(String orderId) {
if (StringUtils.isEmpty(orderId)) {
throw new IllegalArgumentException("Order ID cannot be empty");
}
return true;
}
}
You can mock StringUtils.isEmpty within a scoped block:
import org.mockito.MockedStatic;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@Test
void testProcessOrderRejectsEmptyId() {
try (MockedStatic<StringUtils> mocked = mockStatic(StringUtils.class)) {
mocked.when(() -> StringUtils.isEmpty("")).thenReturn(true);
OrderService orderService = new OrderService();
assertThrows(IllegalArgumentException.class, () -> orderService.processOrder(""));
}
}
The try-with-resources statement is important: it guarantees that the static mock is closed and the original method behavior is restored as soon as the block exits. This prevents the mock from leaking into other tests and causing unpredictable failures.
Dynamic Stubbing with thenAnswer
Sometimes a fixed return value is not enough. You may need the mock to compute its response based on the input arguments, or you may need different behavior on successive invocations. thenAnswer provides this flexibility by letting you supply a lambda that inspects the invocation and returns a value dynamically.
Here is an example where a calculator mock actually performs addition based on its arguments:
@Test
void testCalculatorAddsArguments() {
CalculatorService calculator = mock(CalculatorService.class);
when(calculator.add(anyInt(), anyInt())).thenAnswer(invocation -> {
int a = invocation.getArgument(0);
int b = invocation.getArgument(1);
return a +