Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll cover a common use case of using Mockito ArgumentCaptor in our unit tests.

Alternatively, for other Mockito.verify use cases, see our Mockito Verify Cookbook.

2. Using ArgumentCaptor

ArgumentCaptor allows us to capture an argument passed to a method in order to inspect it. This is especially useful when we can't access the argument outside of the method we'd like to test.

For example, consider an EmailService class with a send method that we'd like to test:

public class EmailService {

    private DeliveryPlatform platform;

    public EmailService(DeliveryPlatform platform) {
        this.platform = platform;
    }

    public void send(String to, String subject, String body, boolean html) {
        Format format = Format.TEXT_ONLY;
        if (html) {
            format = Format.HTML;
        }
        Email email = new Email(to, subject, body);
        email.setFormat(format);
        platform.deliver(email);
    }

    ...
}

In EmailService.send, notice how platform.deliver takes a new Email as an argument. As part of our test, we'd like to check that the format field of the new Email is set to Format.HTML. In order to do this, we need to capture and inspect the argument that is passed to platform.deliver.

Let's see how we can use ArgumentCaptor to help us.

2.1. Set Up the Unit Test

First, let's create our unit test class:

@RunWith(MockitoJUnitRunner.class)
public class EmailServiceUnitTest {

    @Mock
    DeliveryPlatform platform;

    @InjectMocks
    EmailService emailService;
  
    ...
}

We're using the @Mock annotation to mock DeliveryPlatform, which is automatically injected into our EmailService with the @InjectMocks annotation. Refer to our Mockito Annotations article for further details.

2.2. Add an ArgumentCaptor Field

Secondly, let's add a new ArgumentCaptor field of type Email to store our captured argument:

@Captor
ArgumentCaptor<Email> emailCaptor;

2.3. Capture the Argument

Thirdly, let's use Mockito.verify with the ArgumentCaptor to capture the Email:

Mockito.verify(platform).deliver(emailCaptor.capture());

We can then get the captured value and store it as a new Email object:

Email emailCaptorValue = emailCaptor.getValue();

2.4. Inspect the Captured Value

Finally, let's see the whole test with an assert to inspect the captured Email object:

@Test
public void whenDoesSupportHtml_expectHTMLEmailFormat() {
    String to = "[email protected]";
    String subject = "Using ArgumentCaptor";
    String body = "Hey, let'use ArgumentCaptor";

    emailService.send(to, subject, body, true);

    Mockito.verify(platform).deliver(emailCaptor.capture());
    Email value = emailCaptor.getValue();
    assertEquals(Format.HTML, value.getFormat());
}

3. Avoiding Stubbing

Although we can use an ArgumentCaptor with stubbing, we should generally avoid doing so. To clarify, in Mockito, this generally means avoiding using an ArgumentCaptor with Mockito.when. With stubbing, we should use an ArgumentMatcher instead.

Let's look at a couple of reasons why we should avoid stubbing.

3.1. Decreased Test Readability

First, consider a simple test:

Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key");
Mockito.when(platform.authenticate(Mockito.eq(credentials)))
  .thenReturn(AuthenticationStatus.AUTHENTICATED);

assertTrue(emailService.authenticatedSuccessfully(credentials));

Here, we use Mockito.eq(credentials) to specify when the mock should return an object.

Next, consider the same test using an ArgumentCaptor instead:

Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key");
Mockito.when(platform.authenticate(credentialsCaptor.capture()))
  .thenReturn(AuthenticationStatus.AUTHENTICATED);

assertTrue(emailService.authenticatedSuccessfully(credentials));
assertEquals(credentials, credentialsCaptor.getValue());

In contrast to the first test, notice how we have to perform an extra assert on the last line in order to do the same as Mockito.eq(credentials).

Finally, notice how it isn't immediately clear what credentialsCaptor.capture() refers to. This is because we have to create the captor outside of the line we use it on, which reduces readability.

3.2. Reduced Defect Localization

Another reason is that if emailService.authenticatedSuccessfully doesn't call platform.authenticate, we'll get an exception:

org.mockito.exceptions.base.MockitoException: 
No argument value was captured!

This is because our stubbed method hasn't captured an argument. However, the actual issue is not in our test itself, but the actual method we are testing.

In other words, it misdirects us to an exception in the test, whereas the actual defect is in the method we are testing.

4. Conclusion

In this short tutorial, we looked at a general use case of using ArgumentCaptor. We also looked at the reasons for avoiding using ArgumentCaptor with stubbing. As usual, all of our code samples are available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments