Stephen Freeman Rotating Header Image

September, 2012:

On the composeability of Hamcrest matchers

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
  hasReturnCode(final int returnCode) {
    return new TypeSafeDiagnosingMatcher() {
      @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 

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
  hasReturnCode(final Matcher codeMatcher) {
    return new TypeSafeDiagnosingMatcher() {
      @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
hasReturnCode(final Matcher codeMatcher) {
  return new FeatureMatcher(
          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>