In a previous post I described how Hamcrest can save your soul. After writing that, it was pointed out that you probably don’t need to suffer so much boiler-plate to save your soul.

With that thought I set out to write the deeplyIsEqual matcher. The result is the DeepIsEqual Matcher. Using it is pretty easy. In that previous post I described the Matcher for comparing lightsabers, LightsaberIsEqual. We no longer need that. Now we can simply do

assertThat(anakinsLightsaber, is(deeplyEqualTo(lukesFirstLightsaber));

No extra boilerplate is needed and we still get all the benefits of only seeing the fields that didn’t match in the error messages. Oh, and the reported expected value is built using reflection too, so you don’t need to worry about whether the type of objects you’re comparing implement toString or how they implement it.

After going through the initial implementation I started switching over the OAuth tests to use this new matcher. Everything was going great until I hit a case where PublicKey or PrivateKey objects were being compared. The problem was, one was a generated key and the other was converted from an encoded string value. Apparently, depending on which way you create Key objects, the internal fields can be slightly different. So, comparing them recursively was failing. I struggled with what to do – I could add the Key to the internal, hardcoded types that are compared by simply using equals(), or I could add the ability for testers to specify how they wanted certain types to be compared using a MatcherFactory. This being 20% time and me seeing other places where specifying custom matchers for certain types would be very useful, I went with the second option.

So, if we wanted to match Hilts in a very specific way we could that pretty simply.

public static Matcher<? super Lightsaber> equalTo(Lightsaber lightsaber)
{
return deeplyEqualTo(lightsaber, extraTypeMatchers);
}

I wrapped the deeplyEqualTo call in a convience method, because the creation of the extraTypeMatchers can be a bit gnarly and makes the tests a bit harder to read. As a simple example, let’s say we want to compare {{Hilt}}s using their equals() and don’t care if the subtypes are different. To accomplish that we could use the following extraTypeMatchers.

import static com.atlassian.hamcrest.ClassMatchers.isAssignableTo;
import static com.atlassian.hamcrest.DeepIsEqual.deeplyEqualTo;
import static com.atlassian.hamcrest.MatcherFactories.isEqual;
Map<Matcher<Class<?>>, MatcherFactory> extraTypeMatchers = new HashMap<Matcher<Class<?>>, MatcherFactory>()
{{
put(isAssignableTo(Hilt.class), isEqual());
}};

That’s not very pretty, but by wrapping it up our test can go back to simply being

assertThat(anakinsLightsaber, is(equalTo(lukesFirstLightsaber));

and we get all the benefits we had before, plus we are matching Hilts in the desired way.

The biggest thing left to tackle is how to handle object graphs that have cycles in them. We have some ideas about how to do it, such as tracking the objects visited and if we find one that we’ve already visited just stop recursion and assume it will be true, relying on all the other matching to prove otherwise. I’m not 100% sure this will work. The main problem being, what happens if a field in expected value has a reference to, for example, the first object in the graph. But, the actual value doesn’t have a cyclic reference but does have a reference to an object that should be considered “equal” to the same object as the expected value (I know, it’s probably not entirely clear what I mean but I’m having a problem figuring out how to phrase it more better as my brain keeps breaking when I think too hard about it). I’m thinking for now, being able to specify custom matchers for types should be enough. If there is some part of the object graph that we expect to have a cycle, then just specify a custom matcher for it and move on.

To play with this you can checkout the code from the labs project, or you can get from our public maven repository. If you find any problems with it report them in JIRA.

Thanks, and enjoy testing again!