loading...
Cover image for Matt's Tidbits #64 - Digging into some Ham...crest assertions

Matt's Tidbits #64 - Digging into some Ham...crest assertions

mpeng3 profile image Matthew Groves Originally published at Medium ・3 min read

Last week I shared a quick tidbit about Android Gradle Plugin 3.6.2. This time, I'd like to share an interesting error with Hamcrest assertions and how I resolved it!

I was writing some new code (and some unit tests!) this week, and stumbled on a strange error when running my unit tests:

None of the following functions can be called with the arguments supplied:

public open fun <T : Any!> assertThat(p0: (???..???), p1: Matcher<in (???..???)>!): Unit defined in org.hamcrest.MatcherAssert

public open fun assertThat(p0: String!, p1: Boolean): Unit defined in org.hamcrest.MatcherAssert

The code in question looked like this:

This seemed a little puzzling to me - I had used this pattern a thousand times before - why was it failing now?

It turns out, Hamcrest's is matcher requires that the types of both arguments be exactly the same. Another way of saying this is that the is matcher is invariant. What I was expecting is that it would be covariant.

Here's the definition of the is matcher:

public static <T> org.hamcrest.Matcher<T> is(T value) {
  return org.hamcrest.core.Is.<T>is(value);
}

To get the behavior I want, the definition should be:

public static <T> org.hamcrest.Matcher<? extends T> is(T value) {
  return org.hamcrest.core.Is.<T>is(value);
}

In Kotlin, this would use Matcher<out T> instead.

Since I can't change Hamcrest, what are our options?

  1. Add a cast to the is method:
    assertThat(mapContainer.getMap(), is<Map<String, Int>>(map))
    Pros: Quick
    Cons: Doing this results in Android Studio showing this as a lint warning, and offers you the quick fix to "Remove explicit type arguments", which if you do, will no longer compile. You could add an annotation to ignore the lint warning, but I think that will start to get messy very quickly.

  2. Explicitly cast the map to its parent type:
    assertThat(mapContainer.getMap(), is(map as Map<String, Int>))
    Pros: No lint warning!
    Cons: Cumbersome to have to type this out every time.

There are some other options that involve bad practices, so I'm listing them separately here:

  1. Do NOT cast the left side to be the same type as the right side
    assertThat(mapContainer.getMap() as LinkedHashMap<String, Int>, is(map))
    Cons: You're making assumptions that may not be true!

  2. Do NOT swap the arguments (putting the expected type on the left and the actual value on the right)
    assertThat(map, is(mapContainer.getMap()))
    Cons: This makes the error messages you receive from Hamcrest confusing and incorrect.

Do we have any other options?

The answer is… Truth!
https://truth.dev

Truth supports similar capabilities to Hamcrest, but uses chained method calls so the IDE can suggest assertions that apply to the type of data you are working with! Hamcrest often feels frustrating in that you have to just know what the various matchers are.

Here's an example of our check using Truth:

assertThat(mapContainer.getMap()).isEqualTo(map)

This compiles and runs just fine! 🎉

If we look at the code for isEqualTo(), we see that it will accept any type of object on the right-hand side, even things that wouldn't make sense, like the number 5, because the input type is Object (Any? in Kotlin):
public final void isEqualTo(Object other) {...}

Truth was created by Google and is very similar to the popular AssertJ library. However, it has a simpler API and all of Truth's features work out-of-the-box with Android (and it's even being bundled as part of the AndroidX Test library, with helpful assertions for some Android types, such as Bundles, Intents, etc.)

Truth has a helpful article if you're curious in learning more about it and how it compares to Hamcrest and AssertJ: https://truth.dev/comparison

I hope you learned something new today, and will look into Truth for writing assertions in your code! Let me know how it goes in the comments - I would be interested to hear how your experience compares with my own. And, please follow me on Medium if you're interested in being notified of future tidbits.

Interested in joining the awesome team here at Intrepid? We're hiring!

This tidbit was originally delivered on April 10, 2020.

Posted on by:

mpeng3 profile

Matthew Groves

@mpeng3

Software engineer with 10+ years of professional experience in C++, C#, Java, and Kotlin.

Discussion

markdown guide