DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Metal and the Simulated Annealing Algorithm
  • Reactive Kafka With Spring Boot
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • How To Create a Homescreen Widget in Android

Trending

  • The Perfection Trap: Rethinking Parkinson's Law for Modern Engineering Teams
  • Advancing Robot Vision and Control
  • A Guide to Auto-Tagging and Lineage Tracking With OpenMetadata
  • Implementing API Design First in .NET for Efficient Development, Testing, and CI/CD
  1. DZone
  2. Coding
  3. Languages
  4. How to Make Your Own Hamcrest Matchers in Kotlin

How to Make Your Own Hamcrest Matchers in Kotlin

A Match made in Hamcrest-heaven!

By 
Jake Zimmerman user avatar
Jake Zimmerman
·
Jan. 23, 20 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
12.9K Views

Join the DZone community and get the full member experience.

Join For Free

A Match made in Hamcrest-heaven!

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.

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:

Kotlin
 




x


 
1
@Test
2
fun testUsingMatcher() {
3
    val string = methodThatShouldReturnAnEmptyString();
4
    assertThat(string, isEmptyString());
5
}



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 @JvmStatic.

This code for the function is as simple as this:

Kotlin
 




xxxxxxxxxx
1


 
1
fun isEmptyString() = IsEmptyString()



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 Object ( 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 BaseMatcher.

Here's the implementation of our IsEmptyString's matches() method:

Kotlin
 




xxxxxxxxxx
1


 
1
override fun matches(actual: Any?): Boolean =
2
    if (actual is String)
3
        actual == ""
4
    else
5
        false



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() and describeMismatch(). The describeTo() method is used to describe what is expected by the matcher. In this case, that's an empty String. The  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:

Kotlin
 




xxxxxxxxxx
1


 
1
<user-provided reason>
2
Expected: <result of describeTo>
3
     but: <result of describeMismatch>



The user-provided reason can be defined by sending a String argument first into the assertThat() method.

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:

Kotlin
 




xxxxxxxxxx
1


 
1
override fun describe(description: Description) {
2
    description.appendText("empty String")
3
}
4
 
          
5
override fun describeMismatch(item: Any?, description: Description) {
6
    description.appendValue(item)
7
}



You'll note that the Description object had the two methods appendText() and 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 StringBuilder. And 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.

Happy matching!

Further Reading

[DZone Refcard] Getting Started With Kotlin

Kotlin (programming language) Hamcrest

Published at DZone with permission of Jake Zimmerman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Metal and the Simulated Annealing Algorithm
  • Reactive Kafka With Spring Boot
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • How To Create a Homescreen Widget in Android

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!