Unit Testing With Hamcrest Matchers: verifying the contents of a collection
Some Different Approaches To Verifying A Collection Result
Here's one of my favorite techniques that seems to be currently under utilized in our team. It uses Hamcrest Matchers to verify the contents of a collection in a unit test.
APPROACH 1
1
2
3
4
|
import static org.hamcrest.Matchers.containsInAnyOrder;
...
List<String> retrievedNames = ...
assertThat(retrievedNames, containsInAnyOrder("name1", "name2", "name3"));
|
Without using Matchers, a typical approach could be something like this:
APPROACH 2
1
2
3
4
|
List<String> expectedNames = Arrays.asList("name1", "name2", "name3");
List<String> retrievedNames = ...
assertEquals("expectedNames and retrievedNames must match in size.", expectedNames.size(), retrievedNames.size());
assertTrue("expectedNames and retrievedNames must match in contents.", expectedNames.containsAll(retrievedNames));
|
Hamcrest Matchers: The Clarity, Expressiveness and Flexibility of It
If you ask me, using Hamcrest Matchers as in APPROACH 1 is much clearer. Not only that, it is more flexible. Among the pre-built Matchers that come with Hamcrest are the following:
· contains(E...): all elements in both sets must match exactly AND with same order
· containsInAnyOrder(E...): all elements in both sets must match exactly, but iteration order may differ
· hasItems(E... elems): the other set being matched against is a superset of elems.
· isIn(E... elems): the other set being matched against is a subset of elems.
You can even combine matchers in a very readable manner, like this:
assertThat(mutantSequence, either(hasItem("AAAGCGAAA")).or(hasItem("TTTCGCTTT")));
|
Hamcrest Matchers: The Precision and Detail of Its Error Messages
A second major advantage with APPROACH 1 is the msg that is generated when the assertion fails. The message will tell you exactly which element(s) failed to match. E.g.
A Hamcrest Matcher Error Message
Given
// place.uniqueAiTaxonomyIds() actually returns 20,21,22,23
assertThat(place.uniqueAiTaxonomyIds(), containsInAnyOrder(20L,21L,23L));
Assertion failure yields
java.lang.AssertionError:
Expected: iterable over [<20L>, <21L>, <23L>] in any order
but: Not matched: <22L>
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:865)
at org.junit.Assert.assertThat(Assert.java:832)
at com.millennialmedia.ai.core.model.places.PlaceTest.testUniqueAiTaxonomyIds(PlaceTest.java:25)
|
How is that for a life-saving diagnostic msg, esp if you have more than just a handful of elements to compare!!
The equals() Conundrum
The worst approach of all (but sadly one that I see all too often) is the following:
APPROACH 3
1
2
3
4
5
|
List<String> expectedResults = ...
List<String> retrievedResults = ...
assertNotNull(retrievedResults);
assertTrue(retrievedResults.size() > 0);
|
Notice how we limited the test to verifying that the result is not empty. We don't bother (don't dare?) to verify if the contents are as expected. The test is essentially incomplete. But for some reason this seems to be a very common (if not the most common) approach that you'll encounter in our test code base.
Another gripe (albeit a much smaller one) that I have with the previous example is in the way it invokes assertNotNull and assertTrue without providing a message. If the assertion fails, we get a stack trace like the below.
Failed Assertion With No Msg
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertTrue(Assert.java:52)
...
|
The stack trace would look less mysterious and be a lot more helpful if a well thought out failure msg were added to the assertion. This is especially apt given our team's propensity to write tests that assert multiple things, sometimes up to 10+ asserts, in a single test method (which is an issue in and of itself, but topic for another blog).
Going back to my main complaint about APPROACH 3, why are we so timid about verifying the contents of the retrievedResults? A possible reason is that we could not (or do not want to) depend on the result objects' equals() method. Maybe the equals() method has not been overridden (and it is, arguably, dubious to implement the equals() method just to satisfy some testing need). Or maybe the equals() method as implemented does not lend itself to the needs of the test, e.g. it does not compare all the object's properties that we need for the test.
If we could depend on the equals() method, we could have easily used APPROACH 1 or 2. The simplest approach is usually best. But what do we do if it so happens that we could not depend on the equals() method?
I've seen places in our test code (at least 4 or 5 places, but perhaps more) where we address the issue using the following approach:
APPROACH 4
avs = intel.getAudienceValues();
AssertJUnit.assertNotNull(avs);
AssertJUnit.assertEquals(4, avs.size());
for (AudienceValue av : avs)
{
if (av.getAudienceValueId() == parentExplicitAudienceValueId)
{
AssertJUnit.assertEquals(1.0, av.getReferenceValue(), 0);
AssertJUnit.assertEquals(1, av.getFactors().size());
AssertJUnit.assertEquals("value1", av.getFactors().get(0).getValues());
AssertJUnit.assertEquals(10.0, av.getFactors().get(0).getWeight());
}
else if (av.getAudienceValueId() == genderAudienceValueId)
{
AssertJUnit.assertEquals(1.0, av.getReferenceValue(), 0);
AssertJUnit.assertEquals(1, av.getFactors().size());
AssertJUnit.assertEquals("value2", av.getFactors().get(0).getValues());
AssertJUnit.assertEquals(20.0, av.getFactors().get(0).getWeight());
}
else if (av.getAudienceValueId() == parentAudienceValueId)
{
AssertJUnit.assertEquals(1.0, av.getReferenceValue(), 0);
AssertJUnit.assertEquals(1, av.getFactors().size());
AssertJUnit.assertEquals("PARENT_EXP=PROUD_PARENT", av.getFactors().get(0).getValues());
AssertJUnit.assertEquals(1.0, av.getFactors().get(0).getWeight());
}
else if (av.getAudienceValueId() == femaleAudienceValueId)
{
AssertJUnit.assertEquals(1.0, av.getReferenceValue(), 0);
AssertJUnit.assertEquals(1, av.getFactors().size());
AssertJUnit.assertEquals("GENDER=FEMALE", av.getFactors().get(0).getValues());
AssertJUnit.assertEquals(1.0, av.getFactors().get(0).getWeight());
}
else
{
AssertJUnit.fail("unknown audience for deviceId1 with avId = [" + av.getAudienceValueId() + "]");
}
|
So this approach seems to be comparing only selected properties within the AudienceValue objects. However, this approach suffers from a couple of issues. First, is readability, maintainability, and the repetition of similar comparison logic across each type of AV object (which also makes it more error prone).
The second issue is that this code may actually contain a (not so subtle) bug. Examining the code, it seems one assumption is that the collection "avs" contains exactly 4 elements (hence the assertEquals(4, avs.size()). That's ok. Another assumption seems to be that the elements in the "avs" collection may be in any particular order, hence the use of the for loop. So far so good. Lastly, it seems that the intent is to test that the "avs" collection contains exactly one instance of each of the 4 types of AV objects. But if avs contained 4 duplicate instances of the same type of AV object, the test would still pass. So the test is not precise enough.
Custom Matchers to the Rescue
What's a better approach? Roll your own Matcher! I was going to provide my own example on how to do this, but seeing that these articles already did such an excellent job, I think I'll just refer you to them:
· How Hamcrest Can Save Your Soul : excellent article explaining what a custom Hamcrest Matcher can do for you, with a full example.
As illustrated by the last of the above articles, by writing a custom Matcher, you gain a couple of valuable things that the equals() method can't buy you. First, when comparing objects defined in 3rd party libraries, you won't be able to override equals() to meet your testing needs. Second, a custom Matcher can provide a very detailed diff msg of exactly which properties do not match. For example, if you depend on the equals() method you get something like this:
Expected: <BIG GLOB OF OBJECT AND FIELD ATTRIBUTES GENERATED WITH ToStringBuilder>
but was: <OTHER BIG GLOB OF OBJECT AND FIELD ATTRIBUTES WITH GENERATED WITH ToStringBuilder>
|
OTOH, by writing your own custom Matcher, you can control the error msg to pinpoint exactly which properties in the two objects differ:
Given
assertThat(lukesFirstLightsaber, is(equalTo(maceWindusLightsaber)));
We get:
Expected: is {singleBladed is true, color is PURPLE, hilt is {size is LARGE, ...}}
but: is {color is GREEN, hilt is {size is MEDIUM}}
|
Way cool, and potentially a great time-saver, esp if the custom Matcher you write is used in several tests and if the object being validated contains lots of fields or is a complex hierarchy of other objects. (Caveat: Keep in mind though that the simplest approach would be to leverage the equals() method if you can, and I suspect that in most cases you should be able to. There's no need to go around writing custom Matchers if the equals() method already serves your need).
In Closing
Finally, I'll end with a few recommendations and tips.
1. I'd recommend using hamcrest-all-1.3, which is the latest version as of this writing (Feb 2013)
2. Use a version of JUnit that matches up to the version of Hamcrest that you are using. JUnit 4.4+ comes with hamcrest-core bundled (classes in hamcrest-core are a subset of those in hamcrest-all). Using incompatible versions of JUnit and hamcrest-all can lead to funky errors, depending on which jar happens to come first on your classpath. If using hamcrest-all-1.3, I suggest using a version of JUnit that corresponds to hamcrest 1.3, such as JUnit 4.10 or 4.11. The other alternative is to use junit-dep-xxx.jar which does not include hamcrest.
3. Hamcrest 1.3 API javadoc can be found here. Surprisingly, it's not that easy to search for it online. (www.jarvana.com also provides the javadocs, but instead of a consolidated set of javadocs, it's split between hamcrest-core and hamcrest-library).
4. Hamcrest is a general purpose matcher library. It's origins lie with the JMock mocking framework, and is now used heavily too with other mocking frameworks such as Mockito/PowerMock and JMockit. Other interesting uses for Hamcrest include implementing easy to grok Regex matching, and implementing functional idioms for Java (see for example the Processing Collections section of this Uses Of Hamcrest page).
5. Another interesting library is Fest. It's more of a fluent assertions library, not really a general purpose matcher library. Here's how Fest compares with Hamcrest.
6. This slide presentation, JUnit Kung Fu: Getting More Out of Your Unit Tests, provides great tips on best practices for using JUnit with Hamcrest, and for unit testing in general. It also provides wonderful graphics that highlight the key points to remember when rolling your own custom Matcher, and gives overviews of advanced Junit features such as Rules, Categories, Parameterized Tests, and Theories.
Hope you enjoy using Hamcrest with JUnit (or perhaps Fest) to improve our unit tests!