Mastering Mockito: Using doAnswer for Complex Mocking Scenarios
In unit testing, sometimes we need more control over the behavior of mocked methods than simple return values. Imagine you have a method that takes a Consumer as an argument and performs a series of actions on it. How do you verify its behavior or simulate different responses for it in a controlled test environment?
This is where Mockito.doAnswer comes in handy! In this article, we’ll walk through how to use doAnswer to customize a mock’s response dynamically, allowing you to verify interactions and simulate real behavior flexibly.
Understanding the Use Case
Suppose we have a BatchJobService class that deletes temporary objects. The runDeleteTemporaryObjects method is invoked through a Consumer to perform this deletion task. Here’s a simplified version of the class:
public class BatchJobService {
private final PrivilegedUser privilegedUser;
public BatchJobService(PrivilegedUser privilegedUser) {
this.privilegedUser = privilegedUser;
}
public void scheduledDeleteTemporaryObjectsJob() {
List<String> tenants = getTenants();
tenants.forEach(this::deleteTemporaryObjectsForTenant);
}
private void deleteTemporaryObjectsForTenant(String tenant) {
privilegedUser.modifyUser(user -> user.setTenant(tenant)).run(this::runDeleteTemporaryObjects);
}
private void runDeleteTemporaryObjects(ChangeSetContext changeSetContext) {
System.out.println("runDeleteTemporaryObjects method invoked");
}
private List<String> getTenants() {
// Simulate getting a list of tenants
return List.of("tenant1", "tenant2");
}
}Code language: Java (java)public class ChangeSetContext {
public <T> void run(Consumer<ChangeSetContext> action) { action.accept(this); }
}Code language: Java (java)public class PrivilegedUser {
<T> void run(Consumer<PrivilegedUser> action) {
action.accept(this);
}
public ChangeSetContext modifyUser(Consumer<User> user) {
System.out.println("user" +user);
user.accept(null);
return new ChangeSetContext();
}
}Code language: Java (java)public class User {
void setTenant(String tenant) {}
}Code language: Java (java)In this setup, deleteTemporaryObjectsForTenant performs a deletion for each tenant by running runDeleteTemporaryObjects within a privileged context, using a Consumer. We’ll need to verify that runDeleteTemporaryObjects is indeed invoked and simulate this behavior without executing actual business logic..
The Problem with Simple Mocks
If we mock privilegedUser.modifyUser(...).run(...), we would ideally want to verify that runDeleteTemporaryObjects is executed for each tenant. But since runDeleteTemporaryObjects is only indirectly triggered by the Consumer‘s accept method, we need to customize the mock’s behavior to call it in a way we can control.
Using Mockito.doAnswer for Custom Mocking
Mockito’s doAnswer allows you to intercept method calls and define a custom response. Here’s how we use it to achieve our goal.
Setting Up the Test
We’ll use doAnswer to simulate the run method on our mocked ChangeSetContext object. Let’s break down the code:
public class BatchJobServiceTest {
BatchJobService batchJobService;
PrivilegedUser privilegedUser;
ChangeSetContext changeSetContext;
@BeforeEach
void setUp() {
privilegedUser = Mockito.mock(PrivilegedUser.class);
batchJobService = new BatchJobService(privilegedUser);
}
@Test
void scheduledDeleteTemporaryObjectsJob() throws Exception {
changeSetContext = Mockito.mock(ChangeSetContext.class);
when(privilegedUser.modifyUser(any())).thenReturn(changeSetContext);
Mockito.doAnswer(
invocation -> {
Consumer<?> consumer = invocation.getArgument(0);
consumer.accept(null);
return null;
})
.when(changeSetContext)
.run(any(Consumer.class));
// Capture console output
String consoleOutput =
SystemLambda.tapSystemOut(
() -> {
batchJobService.scheduledDeleteTemporaryObjectsJob();
});
batchJobService.scheduledDeleteTemporaryObjectsJob();
String expectedOutOut = "runDeleteTemporaryObjects method invoked"+System.lineSeparator()+
"runDeleteTemporaryObjects method invoked";
assertTrue(
consoleOutput.contains(
expectedOutOut),
"Expected message 'runDeleteTemporaryObjects method invoked' was not found in console output.");
}
}Code language: Java (java)Breakdown of doAnswer
Let’s take a closer look at what doAnswer is doing here:
- Intercepting Method Calls:
Mockito.doAnswer(invocation -> { ... }).when(changeSetContext).run(any(Consumer.class));We set up a custom response forrunon our mockchangeSetContext, which accepts anyConsumeras a parameter. - Retrieving and Executing the
Consumer:Consumer<?> consumer = invocation.getArgument(0); consumer.accept(null);invocation.getArgument(0)retrieves theConsumerthat was passed torun. By callingconsumer.accept(null), we simulate the action that theConsumerwould normally perform, allowing us to indirectly triggerrunDeleteTemporaryObjects. - Returning
null:return null;This ensures that therunmethod in the mock returns nothing, as expected.
Validating the Test Output
In ourtest code, we would capture and assert that the message runDeleteTemporaryObjects method invoked printed twice to console.
Sourcecode
You can download source code of blog post from GitHub
Summary
By using Mockito.doAnswer, we gain control over complex interactions with mocks, such as those involving Consumer functions. This approach is powerful for testing scenarios where direct method calls trigger chained operations or background tasks. With doAnswer, you can validate complex execution paths in a clean and controlled manner.