Enhancing Object Matching in Kotlin Component Tests: From Simple Assertions to Hamcrest Matchers

Enhancing Object Matching in Kotlin Component Tests: From Simple Assertions to Hamcrest Matchers

I was writing a component test in Kotlin for a scenario where we publish an event to a Kafka topic whenever we receive a request from an upstream system. It offered a good opportunity to learn how to write a custom matcher for comparing two different types of objects!

For demonstration purposes let's imagine I'm a middleman between a customer wanting a cake and a baker. The cucumber scenario looked a bit like this:

Scenario: Tell the baker that the customer wants some cake
    Given the customer has a cake request
    When the request is submitted
    Then a cake requested event is published to the baker

At some point in my test, I wanted to make sure that certain fields in the outgoing cake requested event matched the ones received in the incoming cake request. Because I was intercepting the outgoing event using a Kafka listener, it was in JsonNode format.

So imagine this incoming CakeRequest:

data class CakeRequest(
    val requestId: String,
    val flavour: String,
    val colour: String,
    val wantsSprinkles: Boolean,
)

And an outgoing JsonNode that looks like this:

{
    "flavour":"chocolate",
    "colour":"red",
    "wantsSprinkles":"true"
}

What is the best way to make sure they have the same values (minus the requestId)?

Simple private method

I started with this simple assertion:

assertTrue { hasSameEssentialValues(cakeRequest, resultingEvent) }

private fun hasSameEssentialValues(cakeRequest: CakeRequest, resultingEvent: JsonNode) =
        cakeRequest.flavour == resultingEvent["flavour"].asText()
                && cakeRequest.colour == resultingEvent["colour"].asText()
                && cakeRequest.wantsSprinkles == resultingEvent["wantsSprinkles"].asBoolean()

While this worked fine, there were a few downsides:

  • The assertion didn't read fantastically

  • It polluted my test file with ugly private methods

  • Most importantly, the truth about how to match fields between a CakeRequest and a JsonNode was being mixed in with everything else.

In steps a Hamcrest matcher!

By the power of Hamcrest, I created an assertion that looks like this:

assertThat(resultingEvent, hasSameEssentialValuesAs(cakeRequest))

And a matcher in a separate file that looked like this:

fun hasSameEssentialValuesAs(cakeRequest: CakeRequest) = CakeRequestMatcher(cakeRequest)

class CakeRequestMatcher(private val cakeRequest: CakeRequest) : BaseMatcher<JsonNode>() {

    override fun describeTo(description: Description) {
        description.appendText("The important fields must match")
    }

    override fun matches(resultingEvent: Any?): Boolean {
        if (resultingEvent !is JsonNode)
            return false

        return cakeRequest.flavour == resultingEvent["flavour"].asText()
                && cakeRequest.colour == resultingEvent["colour"].asText()
                && cakeRequest.wantsSprinkles == resultingEvent["wantsSprinkles"].asBoolean()
    }
}

This makes the assertion read brilliantly and follows the single responsibility principle much better.

But there are some downsides:

  • The mandatory implementation describeTo() - not useful in my situation

  • matches() takes the Any? type as its input, requiring manual type-checking

  • The whole thing is complex - it will be harder to read and maintain

Final Takeaways: Reflections and Next Steps

While this was a good exercise where I learned a lot about matchers (and ended up writing this article), in my specific use case I ultimately decided to check the fields in a simple package-level function in its own file. This kept the responsibility in the correct place and made the whole thing less complex.

It means when someone else (or me in 6 months) reads this test, they'll take less time to understand it, and less time to modify it. Which surely is the ultimate goal when writing code, right?

Update:

5 minutes after I published this article I sent the link to my esteemed work colleague who I worked with on this problem, and he replied straight away with a solution that blows mine out of the water. It solves the problem of readability and allows you to abstract the comparison logic away from both the test and the CakeRequest object:

assertTrue { cakeRequest.hasSameEssentialValuesAs(resultingEvent)

Kotlin has extension functions, duh!!