Intro
I’ve done a few articles about Hamcrest Matchers now, and you may or may not think they’re the bee’s knees, the way I do. But there’s another great contender out there for better, cleaner, and more fluent assertions than the defaults with JUnit: AssertJ. I will admit, AssertJ is a great framework, and it’s certainly useful.
For those of you who don’t know about AssertJ, check out their home page. One of the largest factors going for AssertJ is the chaining of assertions into one nice assertion. For example, you could do something like the following:
assertThat("string to be tested")
.startsWith("str").contains("to").hasLength(19);
Note: I completely made up the assertion calls in the above code. so don’t copy it, hoping that it’ll work as-is without creating your own AssertJ class.
AssertJ can get away with this a lot more easily than Hamcrest can, since the AssertJ object is created with the opening assertThat() method call, taking in the object to test right off the bat. This allows it to contain the object while it makes simple assertions with each method, returning itself to allow for chained calls.
There are a few small downsides to this, though, so you’ll have to decide which you prefer to use (or use both, if you’re ambitious).
Chaining Assertions
So, it turns out that this can be kind of a pain in the butt to do. Luckily for you, I’ve made it easier. I’ve created 2 base classes (a type safe version, and a regular version), and can be found here. You may notice that this is a folder in a fork of the Hamcrest library. That’s because I’ve submitted a pull request with these classes. They’ve been approved, but still need to be merged in. It’s very exciting for me.
To make your own chainable Matchers, all you have to do is create an abstract base class that extends one of the two base classes provided. For example:
public abstract class ChainableStringMatcher extends ChainableTypeSafeMatcher {
protected ChainableStringMatcher(ChainableStringMatcher decoratedMatcher) {
super(decoratedMatcher);
}
}
You’ll note that the type taken into the constructor is of ChainableStringMatcher, not ChainableTypeSafeMatcher. This isn’t strictly necessary, but it helps to restrict all chaining calls to subclasses of ChainableStringMatcher.
The next thing you’ll need to do is make your regular Matchers, extending from your abstract class. In this case, from ChainableStringMatcher.
public class StartsWith extends ChainableStringMatcher
{
private String expectedPrefix;
public static ChainableStringMatcher startsWith(String expectedPrefix) {
return new StartsWith(null, expectedPrefix);
}
protected StartsWith(ChainableStringMatcher decoratedMatcher, String expectedPrefix) {
super(decoratedMatcher);
this.expectedPrefix = expectedPrefix;
}
@Override
public void chainDescribeTo(Description description) {
description.appendText("starts with ")
.appendText(expectedPrefix);
}
@Override
protected boolean chainMatches(String item) {
return item.startsWith(expectedPrefix);
}
@Override
protected void chainDescribeMismatch(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ")
.appendText(item);
}
}
public class Contains extends ChainableStringMatcher {
private String expectedSubstring;
protected Contains(ChainableStringMatcher decoratedMatcher, String expectedSubstring) {
super(decoratedMatcher);
this.expectedSubstring = expectedSubstring;
}
public static ChainableStringMatcher contains(String substring) {
return new Contains(null, substring);
}
@Override
public void chainDescribeTo(Description description) {
description.appendText("contains ")
.appendText(expectedSubstring);
}
@Override
protected boolean chainMatches(String item) {
return item.contains(expectedSubstring);
}
@Override
protected void chainDescribeMismatch(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ")
.appendText(item);
}
}
public class HasLength extends ChainableStringMatcher {
private int expectedLength;
protected HasLength(ChainableStringMatcher decoratedMatcher, int expectedLength) {
super(decoratedMatcher);
this.expectedLength = expectedLength;
}
public static ChainableStringMatcher hasLength(int expectedLength) {
return new HasLength(null, expectedLength);
}
@Override
public void chainDescribeTo(Description description) {
description.appendText("length of ")
.appendValue(expectedLength);
}
@Override
protected boolean chainMatches(String item) {
return item.length() == expectedLength;
}
@Override
protected void chainDescribeMismatch(String item, Description mismatchDescription) {
mismatchDescription.appendText("had length of ")
.appendValue(item.length());
}
}
You’ll note that they look very similar to any other kind of Hamcrest Matcher, except that the methods that they override start with “chain”.
Also, they require special constructors that take in the decorated Matcher (passing in null in their factory methods, indicating that they are the “base” object when called via that). The constructors must also be at least package-private if in the same package as the your chainable superclass, or public if in a different package. You’ll see why in the next step.
You’re almost done. This last step requires you to add some more methods to your chainable superclass. These methods are the ones that are making the decorating calls and allowing you to chain your Matchers together.
public abstract class ChainableStringMatcher extends ChainableTypeSafeMatcher {
protected ChainableStringMatcher(ChainableStringMatcher decoratedMatcher) {
super(decoratedMatcher);
}
public ChainableStringMatcher startsWith_(String expectedPrefix) {
return new StartsWith(this, expectedPrefix);
}
public ChainableStringMatcher contains_(String expectedSubstring) {
return new Contains(this, expectedSubstring);
}
public ChainableStringMatcher hasLength_(int expectedLength) {
return new HasLength(this, expectedLength);
}
}
Done! You may notice that the names of the chaining methods all end with an underscore. This is because it causes a conflict with the names of the factory methods. The two cannot be the same, unfortunately. Probably another good way around it is to capitalize the names of the factories so they resemble constructors more, but you’ll get a warning that says that it looks like a constructor. This isn’t really a problem, but warnings are annoying.
Additional Options
One thing you can do to keep your life simpler is to move/copy all your factories into the chainable superclass, making them accessible from one place. In fact, if you include all Matchers that can apply to a certain type of object (like String in our example) in one class, then you only need to do a static import of that one class to have access to all the factories. For our example, the ChainableStringMatcher would gain all the factory methods of StartsWith, Contains, and HasLength. At that point, you can remove the factories from the individual classes and even make the classes package-private, if they’re in the same package. Then the users only ever need to interact with ChainableStringMatcher.
Another thing that can be done is to have a starter Matcher. For example, if you wanted a set of Matchers for working with exceptions, you might want to do an assert like this:
assertThat( () -> methodThatCanThrow(), throwsA(RuntimeException.class).withAMessage());
You’d probably want to make sure that throwsA() is always called first in this chain, to make sure that an exception is thrown and that it’s the right type.
In order to accomplish that, all you need to do is make it so that the Matcher for throwsA() is the only Matcher in the group of chainable Matchers with publicly available factory methods. It’s really that simple.
Benefits Over AssertJ
While being slightly more difficult to implement than AssertJ chained assertions, chainable Hamcrest Matchers provide some very nice benefits over them. First off, since the AssertionError isn’t thrown until the base matches() call returns false, the Matcher is actually able to make every matches() call and keep track of every single failure to match, rather than just the first failed assertion. AssertJ can do this too, but you need to create a SoftAssertions object from which to make all your assertions from, which leads to more code per assertion, having to prefix your assertions with the name of that object.
Along this same line, the chainable Matchers are also designed to go through, in the event of a failed assertion, and describe everything that was right and everything that was wrong. Again, SoftAssertions can do this too, but it requires more code per assertion.
The last benefit over AssertJ is the last bit in Additional Options, where you can force which Matcher comes first. I doubt there are many instances where this will be needed, but should the need arise, the option is easily available.
Outro
So, if you were a user of AssertJ looking into Hamcrest or vice versa, you should know that Hamcrest can now offer you pretty much everything AssertJ can and more. If you want, you can use both, but that could potentially lead to some confusing code. I’m very satisfied with the way Hamcrest works and will stick with it until something else clearly blows it out of the water. That doesn’t mean I won’t be looking around and learning how to make Hamcrest better, though.
I hope that, when you decide you want to make custom Matchers for your home-brewed class(es), you’ll look into making them chainable with each other to make multiple checks on it when testing it.
Pingback: Redesigning Hamcrest | Programming Ideas With Jake