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 forrun
on our mockchangeSetContext
, which accepts anyConsumer
as a parameter. - Retrieving and Executing the
Consumer
:Consumer<?> consumer = invocation.getArgument(0); consumer.accept(null);
invocation.getArgument(0)
retrieves theConsumer
that was passed torun
. By callingconsumer.accept(null)
, we simulate the action that theConsumer
would normally perform, allowing us to indirectly triggerrunDeleteTemporaryObjects
. - Returning
null
:return null;
This ensures that therun
method 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.