Assertion chaining with Hamcrest

In some cases we want to assert more than one thing in a test. For instance when testing a financial application with a chart of accounts that contains multiple calculated fields. Those fields change after any given mutation. We want to check all of them at once. The problem is that checking more than one thing in a test can lead to a test that is partially skipped. For example, given we have a test with three assertions, when the first assertion fails, then the other two will not be executed. Is there a safe way to chain assertions? The answer is yes, and in this article we will see how to facilitate safe assertion chaining.

Because we do not have the previously mentioned financial application at our disposal, we will use the example of the speed camera like we did when customising our assertions. Let’s start with creating a class that contains two lists. One for collecting matchers, the other for collecting mismatches.

public class BaseMatcherCollector<T> extends BaseMatcher<T> {
    
    private final List<BaseMatcher<? super T>> matchers = 
        new ArrayList<BaseMatcher<? super T>>();
    private final List<BaseMatcher<? super T>> mismatches = 
        new ArrayList<BaseMatcher<? super T>>();
    

The T indicates a generic type. We want to set the type of the BaseMatcherCollector later, so we use BaseMatcherCollector<T> to keep that flexibility. The ? super T is a lower bounded wildcard indicating that we expect an unknown value of type T or a superclass of type T in our lists. Simply put, given we have a class SpeedCamera which is a subclass of MeasuringDevice, when our BaseMatcherCollector is of type SpeedCamera, then we can use matchers of type SpeedCamera and MeasuringDevice. More on generics and wildcards on the oracle website.

We must implement matches and describeTo because we extend the BaseMatcher class.

public boolean matches(Object itemToMatch) {
        for (final BaseMatcher<? super T> matcher : matchers) {
                if (!matcher.matches(itemToMatch)) {
                        mismatches.add(matcher);  
                }
        }
        return mismatches.isEmpty();
}

public void describeTo(Description description) {
        description.appendList("\n", "\n" + "AND" + " ", "", matchers);
}

Our matches method checks the results of every matcher existing in our list matchers. If there is a mismatch, it adds that mismatch to the list of mismatches. After the results of all the matchers have been checked, the method returns true or false based on whether the list of mismatches is empty. Our describeTo method takes care of presenting the expected results.

Now we add a describeMismatch. This is a method in the BaseMatcher class that we override.

@Override
public void describeMismatch(final Object item, final Description description) {
        for (final BaseMatcher<? super T> mismatch : mismatches) {
                description.appendText("\n")
                           .appendDescriptionOf(mismatch).appendText(" BUT ");
                mismatch.describeMismatch(item, description);
        }
}

This method takes care of presenting the test failure information, if present. The next thing on the list is adding a private constructor.

private BaseMatcherCollector(final BaseMatcher<? super T> matcher) {
        matchers.add(matcher);
}

The constructor adds a matcher to the list of matchers. We make a call to this constructor in our next method.

public static <T> BaseMatcherCollector<T> chain(final BaseMatcher<? super T> matcher) {
        return new BaseMatcherCollector<T>(matcher);
}

With this method we create an instance of the BaseMatcherCollector of type T and add a matcher which is of type T or a superclass of T. To allow for more than one matcher, we write a method that simply adds a matcher to the list.

public BaseMatcherCollector<T> and(final BaseMatcher<? super T> matcher) {
        matchers.add(matcher);
        return this;
}

Now let’s see our BaseMatcherCollector at work in a test.

public void oneRingToRuleThemAll() {
        expectedInList.add(49);
        vehicle.passSpeedCamera(53);
        assertThat(speedCamera, chain(
                                hasMeasuredSpeed(53))
                                .and(hasCorrectedSpeedTo(49))
                                .and(hasCorrectedSpeedsInList
                                    (expectedInList))
                                .and(hasTakenAPicture(false))
                                .and(hasRevokedLicense(false))
                                .and(isMeasuringDeviceTypeNamed
                                    ("SpeedCamera")));
}

Note that all matchers are of type SpeedCamera except for the last one. The last matcher isMeasuringDeviceTypeNamed is a matcher of type MeasuringDevice which is a superclass of SpeedCamera. Our implementation allows for that matcher to be chained because we use a lower bounded wildcard. Now let’s see an example in which all matchers return false.

public void noneShallPass() {
        vehicle.passSpeedCamera(53);
        assertThat(speedCamera, chain(
                                hasTakenAPicture(true))
                                .and(hasMeasuredSpeed(99))
                                .and(hasRevokedLicense(true))
                                .and(hasCorrectedSpeedTo(0)));
}
// failure output 
// Taking a picture should have returned <true>
// AND The camera should have corrected the speed to <99>
// AND Revoking license should have returned <true>
// AND The camera should have corrected the speed to <0>
//         but: 
// Taking a picture should have returned <true> BUT  returned <false>
// The camera should have corrected the speed to <99> BUT  alas, the camera corrected it to <53>
// Revoking license should have returned <true> BUT  returned <false>
// The camera should have corrected the speed to <0> BUT  alas, the camera corrected it to <49>

All of our matchers have been executed! And we have enough information about what went wrong. If you are interested in exploring the BaseMatcherCollector, you can clone/download an example project on GitHub.

A disclaimer for using this solution. The creator of the Hamcrest library pointed out it will not work when used with jMock or Mockito because they assume that matchers are immutable. But apart from that, this solution works fine.

Author: David Baak

To code or not to code? That's true.

Leave a Reply

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