Charles Spencer Charles Spencer - 3 months ago 19
Java Question

How can I create a custom ArgumentMatcher that accepts arguments from other matchers?

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?

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+](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html#describeMismatch(java.lang.Object, org.hamcrest.Description)) 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.)