A recent discussion on the hamcrest java users list got me thinking that I should write up a little style guide, in particular about how to create custom Hamcrest matchers.
Reporting negative scenarios
The issue, as raised by “rdekleijn”, was that he wasn’t getting useful error messages when testing a negative scenario. The original version looked something like this, including a custom matcher:
public class OnComposabilityExampleTest { @Test public void wasNotAcceptedByThisCall() { assertThat(theObjectReturnedByTheCall(), not(hasReturnCode(HTTP_ACCEPTED))); } private Matcher<ThingWithReturnCode> hasReturnCode(final int returnCode) { return new TypeSafeDiagnosingMatcher<ThingWithReturnCode>() { @Override protected boolean matchesSafely(ThingWithReturnCode actual, Description mismatch) { final int returnCode = actual.getReturnCode(); if (expectedReturnCode != returnCode) { mismatch.appendText("return code was ") .appendValue(returnCode); return false; } return true; } @Override public void describeTo(Description description) { description.appendText("a ThingWithReturnCode equal to ") .appendValue(returnCode); } }; } }
which produces an unhelpful error because the received object doesn’t have a readable toString()
method.
java.lang.AssertionError: Expected: not a ThingWithReturnCode equal to <202> but: was <ThingWithReturnCode@19d009b4>
The problem is that the not()
matcher only knows that the matcher it wraps has accepted the value. It can’t ask for a mismatch description from the internal matcher because at that level the value has actually matched. This is probably a design flaw in Hamcrest (an early version had a way to extract a printable representation of the thing being checked), but we can use this moment to think about improving the design of the test. We can work with Hamcrest which is designed to be very composeable.
Separating concerns
The first thing to notice is that the custom matcher is doing too much, it’s extracting the value and checking that it matches. A better design would be to split the two activities and delegate the decision about the validity of the return code to an inner matcher.
public class OnComposabilityExampleTest { @Test public void wasNotAcceptedByThisCall() { assertThat(theObjectReturnedByTheCall(), hasReturnCode(not(equalTo(HTTP_ACCEPTED)))); } private Matcher<ThingWithReturnCode> hasReturnCode(final Matcher<Integer> codeMatcher) { return new TypeSafeDiagnosingMatcher<ThingWithReturnCode>() { @Override protected boolean matchesSafely(ThingWithReturnCode actual, Description mismatch) { final int returnCode = actual.getReturnCode(); if (!codeMatcher.matches(returnCode)) { mismatch.appendText(" return code "); codeMatcher.describeMismatch(returnCode, mismatch); return false; } return true; } @Override public void describeTo(Description description) { description.appendText("a ThingWithReturnCode with code ") .appendDescriptionOf(codeMatcher); } }; } }
which gives the much clearer error:
java.lang.AssertionError: Expected: a ThingWithReturnCode with code not <202> but: return code was <202>
Now the assertion line in the test reads better, and we have the flexibility to make assertions such as hasReturnCode(greaterThan(25))
without changing our custom matcher.
Built-in support
This is such a common situation that we’ve included some infrastructure in the Hamcrest libraries to make it easier. There’s a template FeatureMatcher
, which extracts a “feature” from an object and passes it to a matcher. In this case, it would look like:
private Matcher<ThingWithReturnCode> hasReturnCode(final Matcher<Integer> codeMatcher) { return new FeatureMatcher<ThingWithReturnCode, Integer>( codeMatcher, "ThingWithReturnCode with code", "code") { @Override protected Integer featureValueOf(ThingWithReturnCode actual) { return actual.getReturnCode(); } }; }
and produces an error:
java.lang.AssertionError: Expected: ThingWithReturnCode with code not <202> but: code was <202>
The FeatureMatcher
handles the checking of the extracted value and the reporting.
Finally, in this case, getReturnCode()
conforms to Java’s bean format so, if you don’t mind that the method reference is not statically checked, the simplest thing would be to avoid writing a custom matcher and use a PropertyMatcher
instead.
public class OnComposabilityExampleTest { @Test public void wasNotAcceptedByThisCall() { assertThat(theObjectReturnedByTheCall(), hasProperty("returnCode", not(equalTo(HTTP_ACCEPTED)))); } }
which gives the error:
java.lang.AssertionError: Expected: hasProperty("returnCode", not <202>) but: property 'returnCode' was <202>