Verifying parameters of mocks with Mockito using ArgumentMatcher and ArgumentCaptor


December 8th 2021


I've been using Junit and Mockito for years, but I recently hit a case where I needed to verify some of the properties on a parameter passed to a mock and I was surprised when I didn't readily know how to do it. A half hour with StackOverflow and I was good to go, but I thought I'd write this post to capture what I learned.

My basic situation was something like this made-up example: I have a this UsageService that calculates some user usage data, and if usage is high, it sends an email. In my test, I want to make sure that (a) the email is sent and (b) the email has the right "to", "subject", and "body".

Quick caveat before going further: yes, it could be argued that the design itself is bad - for instance, why is there no method that just returns an Email object that could be tested? Ideally we could just refactor the code under test to make it more testable, but in many situations (like with legacy code or when using some framework) it's not always possible to do this, and so verifying parameters like this is still a useful thing to do.

Ok, back to the example. So setting up the unit test, data members for the class under test and then its two dependencies are defined:

public class UsageServiceTest {

    UsageService usageService;

    EmailService emailService;

    UserDAO userDAO;

    ...
}

...and then in the setup() they are initialized, with a mock to return a fake email address.

@BeforeEach
 public void setup() {
    emailService = mock(EmailService.class);
    userDAO = mock(UserDAO.class);

    usageService = new UsageService(emailService, userDAO);

    when(userDAO.fetchEmail(1)).thenReturn("ben@gmail.com");
}

Now the simplest thing to do would be to just verify that sendEmail() was called once by calcUsage() for a particular data set passed to it.

@Test
public void testCalcUsage_times() {
    usageService.calcUsage(1, List.of(5, 10, 50));

    verify(emailService, times(1)).sendEmail(any());
}

...but that's not all I want to do. Additionally, I need to make sure that the email sent was to the right user, had the right subject, and a particular number (usage) in the body of it. What makes this tricky is that this Email is hidden to the client of UsageService, and so I need to test it via the EmailService mock. To do this I found two options.

First is to use an ArgumentMatcher to ensure that the parameter passed to sendEmail() has certain properties.

@Test
public void testCalcUsage_argumentMatcher() {
    usageService.calcUsage(1, List.of(5, 10, 50));

    verify(emailService, times(1)).sendEmail(argThat((email) -> {
        boolean toMatches = "ben@gmail.com".equals(email.getTo());
        boolean subjectMatches = "Alert!".equalsIgnoreCase(email.getSubject());
        boolean bodyMatches = email.getBody()
                                   .contains("65");
        return toMatches && subjectMatches && bodyMatches;
    }));
}

Note that I'm using ArgumentMatcher is a a functional interface, as the parameter to argThat(). This is a quick way to verify that the Email sent to the EmailService is what is expected.

There's another way to achieve this with Mockito, and that's to use a the ArgumentCaptor:

@Test
public void testCalcUsage_argumentCaptor() {
    usageService.calcUsage(1, List.of(5, 10, 50));

    ArgumentCaptor<Email> argument = ArgumentCaptor.forClass(Email.class);

    verify(emailService, times(1)).sendEmail(argument.capture());

    Email email = argument.getValue();
    assertEquals("ben@gmail.com", email.getTo());
    assertEquals("Alert!", email.getSubject());
    assertTrue(email.getBody()
                    .contains("65"));
}

There are instances I could imagine where it'd be helpful to capture the parameter passed like this, for example, if you wanted to log it in the test. In general though, they just seem like two ways to skin the same cat.

Now to take this example a little further, it's possible that EmailService is to be called multiple times, and I want to verify that each call is what I expect (e.g. an email is sent to user 1 and user 2, but not user 3). In this case, it might be helpful to implement the ArgumentMatcher class like this, so that it can be reused:

class EmailArgumentMatcher implements ArgumentMatcher<Email> {

    private final String to;
    private final String subject;
    private final int amount;

    public EmailArgumentMatcher(String to, String subject, int amount) {
        this.to = to;
        this.subject = subject;
        this.amount = amount;
    }

    @Override
    public boolean matches(Email email) {
        boolean toMatches = to.equals(email.getTo());
        boolean subjectMatches = subject.equalsIgnoreCase(email.getSubject());
        boolean bodyMatches = email.getBody()
                                   .contains(amount + "");
        return toMatches && subjectMatches && bodyMatches;
    }
}

...and then to verify the multiple parameters:

@Test
public void testCalcUsage_multiUser() {
    Map<Integer, List<Integer>> userAmounts = new HashMap<>();
    userAmounts.put(1, List.of(5, 10, 50));
    userAmounts.put(2, List.of(20, 60));
    userAmounts.put(3, List.of(10, 5, 5));
    usageService.calcUsage(userAmounts);

    verify(emailService, times(2)).sendEmail(any());

    ArgumentMatcher<Email> user1Matcher = new EmailArgumentMatcher("ben@gmail.com", "Alert!", 65);
    ArgumentMatcher<Email> user2Matcher = new EmailArgumentMatcher("joe@gmail.com", "Alert!", 80);

    verify(emailService, times(1)).sendEmail(argThat(user1Matcher));
    verify(emailService, times(1)).sendEmail(argThat(user2Matcher));
        
    verify(emailService, never()).sendEmail(argThat((email) -> { 
                                  "emily@gmail.com".equalsIgnoreCase(email.getTo())
    }));
}

And that's all I got! Hope this helps someone out there.

I'm an "old" programmer who has been blogging for almost 20 years now. In 2017, I started Highline Solutions, a consulting company that helps with software architecture and full-stack development. I have two degrees from Carnegie Mellon University, one practical (Information and Decision Systems) and one not so much (Philosophy - thesis here). Pittsburgh, PA is my home where I live with my wife and 3 energetic boys.
I recently released a web app called TechRez, a "better resume for tech". The idea is that instead of sending out the same-old static PDF resume that's jam packed with buzz words and spans multiple pages, you can create a TechRez, which is modern, visual, and interactive. Try it out for free!
Got a Comment?
Comments (0)

 None so far!