Steve Freeman Rotating Header Image

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>

5 Comments

  1. Geert says:

    Hi Steve, interesting blog post, thanks!

    One small comment; there’s a typo here:

    ‘greaterThat(25)’ should be ‘greaterThan(25)’

    Kind regards,

    Geert

  2. @geert Thanks. Fixed

  3. David says:

    Useful blog post. In the TypeSafeDiagnosingMatcher code, is there a reason you are adding a space in front of the mismatch text (e.g. so it can be joined to another description) or was it just a typo?

    mismatch.appendText(" return code ");

  4. Kenny says:

    A useful clarification of good style, thanks Steve. It would be more useful still, I think, if the example included how to best represent multiple contained matchers, rather than just a single one.

  5. Jayesh Lalwani says:

    FeatureMatcher reduced my code.. thanks

    How will you compare arrays?. Let’s say I have a bean that contains an string array. I want to match the 2 arrays and if there is a mismatch, I was to print “Element %d was expected to be %s but was %s” How do I do that?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>