|

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:

  1. Intercepting Method Calls: Mockito.doAnswer(invocation -> { ... }).when(changeSetContext).run(any(Consumer.class)); We set up a custom response for run on our mock changeSetContext, which accepts any Consumer as a parameter.
  2. Retrieving and Executing the Consumer: Consumer<?> consumer = invocation.getArgument(0); consumer.accept(null); invocation.getArgument(0) retrieves the Consumer that was passed to run. By calling consumer.accept(null), we simulate the action that the Consumer would normally perform, allowing us to indirectly trigger runDeleteTemporaryObjects.
  3. Returning null: return null; This ensures that the run 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.

Similar Posts