Java – How to create a custom ArgumentMatcher that accepts arguments from other matchers

javamockito

I'm currently writing unit tests on a code base that uses a lot of ActionEvent's internally, and since ActionEvent doesn't override equals(), I'm creating a custom ArgumentMatcher to match ActionEvent's.

My ArgumentMatcher currently looks like this:

public class ActionEventMatcher extends ArgumentMatcher<ActionEvent> {
    private Object source;
    private int id;
    private String actionCommand;

    public ActionEventMatcher(Object source, int id, String actionCommand) {
        this.source = source;
        this.id = id;
        this.actionCommand = actionCommand;
    }

    @Override
    public boolean matches(Object argument) {
        if (!(argument instanceof ActionEvent))
            return false;
        ActionEvent e = (ActionEvent)argument;

        return source.equals(e.getSource()) &&
            id == e.getId() && actionCommand.equals(e.getActionCommand());
    }
}

I would like to know if it possible to change my ArgumentMatcher so that it accepts arguments that were created from other matchers. For instance, if I have a method called actionEvent() that returns the matcher, I would like to be able to do the following:

verify(mock).fireEvent(argThat(actionEvent(same(source), anyInt(), eq("actionCommand"))));

Is there a way to have a custom ArgumentMatcher that accepts arguments from other matchers this way?

Best Answer

You'll be able to do that for Hamcrest or Hamcrest-style matchers, but not for Mockito matchers you get from the static methods on org.mockito.Matchers.

In short, methods like same, anyInt, and eq in Mockito are all designed to fit into method calls in when and verify, so they work counterintuitively through side effects. This makes it really hard to consume them and work with them outside of Mockito internals. By contrast, if you limit yourself to using either Matcher (Hamcrest) or ArgumentMatcher (Mockito) instances, you can manipulate those to your heart's content, and with Hamcrest you'll already have a large library of matchers to start with. See my other Q&A here for context.

In short, your matcher would probably look like this.

public class ActionEventMatcher extends ArgumentMatcher<ActionEvent> {
    /* fields here */

    public ActionEventMatcher(
            Matcher<Object> sourceMatcher,
            Matcher<Integer> idMatcher,
            Matcher<String> actionCommandMatcher) { /* save fields here */ }

    @Override
    public boolean matches(Object argument) {
        if (!(argument instanceof ActionEvent))
            return false;
        ActionEvent e = (ActionEvent)argument;

        return this.sourceMatcher.matches(e.getSource())
            && this.idMatcher.matches(e.getId())
            && this.actionCommandMatcher.matches(e.getActionCommand());
    }
}

As a bonus, you can use describeMismatch in Hamcrest 1.3+ to summarize mismatched fields, which might make it easier to determine which aspects of an ActionEvent are failing. (This won't help for verify calls if you don't use an ArgumentCaptor, though, because Mockito treats a non-matching call as "missing A but received B", not "received B but it failed matcher A for these reasons". You'd have to capture the event and use assertEquals to benefit from mismatch descriptions.)