How to Make Your Own Hamcrest Matchers in Kotlin
How to Make Your Own Hamcrest Matchers in Kotlin
A Match made in Hamcrest-heaven!
Join the DZone community and get the full member experience.Join For Free
Intro to Hamcrest Matchers
First things first, I should quickly explain what a Hamcrest Matcher is. When conducting unit tests, the built-in assertion types that come with the testing framework are generally pretty limited. They make it very easy for a person to end up with multiple asserts to essentially check one thing. Even if it doesn't contain multiple asserts, those asserts aren't the most fluent to read and don't tell exactly what you're checking.
You may also like: Hamcrest Containing Matchers
That's where Hamcrest Matchers come in (and other assertion libraries, but we're looking at Hamcrest right now). They allow you to define your own more robust and more fluent assertions, essentially. For example, if you were testing whether a method correctly returns an empty String, that test might look something like this:
Look at that assertion line. It almost reads like English once you ignore all the "punctuation". "Assert that string is an empty string." Some matchers put more effort into reading like English than others, but they all read well enough to make them easy to understand.
Anatomy of a Hamcrest Matcher
We'll go through the basics, recreating the
IsEmptyString matcher along the way. First off, this matcher will extend from <code-BaseMatcher. With that, let's start building.
There are basically four parts to a Hamcrest Matcher: a static factory method, an assertion, a description of a passed assertion, and a description of a failed assertion. The last two are the biggest reason why I wrote this article. It took me a while with a fair bit of tinkering to figure out the "best practices" for that. But we'll start at the top.
In all honesty, the static factory method isn't an issue in Kotlin. the primary reason for it was to not need the
new keyword, but Kotlin doesn't use it anyway. If you're planning on making it backward compatible for use in Java code, too, then I recommend still using it. If not, then the only thing you need to contend with is capitalization. Are you okay with a capitalized class name being used in the assertion? If so, then you've got it easy. If not, you have the option of either making the class name lower-cased (thus breaking convention) or adding a static factory method. There's also always the
import ... as ... option that Kotlin so graciously provides.
If you're going to make the static factory, I actually recommend doing it as a top-level function, rather than as a
companion object. This makes it automatically a static method for Java users, so they can do a nice static import, and it allows you to avoid the strange companion syntax (especially paired with
This code for the function is as simple as this:
Overall, the method is very bland and obvious. I have yet to run into an instance where it's not. It's also especially quick and easy with all the shortcuts Kotlin allows, such as skipping the return type and using the single expression form.
Next, we need the assertion, which is done with the
matches() method. This is the real work of a matcher, running the actual check. Notice that it's not meant to throw the
AssertionError; that's the job of the
assertThat() method. This method simply returns a
Boolean stating whether the input matches the idea the Matcher tests for.
The input into this method comes from the first argument in the
assertThat() method. When
assertThat() runs the provided matcher's
matches() method, it passes that argument into it. The
matches() method takes in an
Any in Kotlin), not the type specified by the generics. This is because of some weird "feature" of Java generics that I haven't looked into, so I can't explain it. I just wanted you to know that you will generally have to do some sort of type checking when you just extend
Here's the implementation of our
That's all there is to it in this case — it just checks whether it's an empty String.
Now, we have to do the two descriptions. We'll do these in tandem since they're similar. The two descriptions are
describeTo() method is used to describe what is expected by the matcher. In this case, that's an empty
describeMismatch() method is for describing the actual result, usually just outputting the given object.
The two description methods only come into play if the match fails and the
AssertionError is thrown. The
assertThat() method then builds a Description for the failed assertion. It is formatted as follows:
The user-provided reason can be defined by sending a String argument first into the
As you can see, the formatting of the output is such that a person doesn't need to include any extraneous information in the descriptions, such as whether it is describing a match or mismatch.
What's weird about these two methods is that they don't use
Strings, per se. They use what's called a
Description object, which I'm pretty sure is what allows them to 1) avoid excessive
String concatenation, since it seems to work a bit like a
StringBuilder and 2) potentially use different kinds of
Description (which is an interface) objects to display things a little differently in different tools.
So, here's our implementation of those "describe" methods:
You'll note that the
Description object had the two methods
appendValue(). I've only ever used these two, even though it has a few more. Those are for a few rarer cases, and you should check them out if you're interested. If it wasn't clear,
appendText will take a
String and append it to the end of whatever's already in the description, just like a
appendValue appears to run
toString() on whatever you pass in and append that.
Until Next Time
That's it! You're done. You now have a working Hamcrest Matcher. For any more information, you should check out the official website. If you were to do your research, though, you'd discover that we didn't recreate the built-in
IsEmptyString Matcher properly. There are a few techniques that are a little more advanced (and that would make this post get longer than I'm comfortable with). I will go over them in my next post.
Published at DZone with permission of Jake Zimmerman , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.