Everyone loves writing unit tests, don’t you? If your answer is “no” you should have a look into Hamcrest. Hamcrest, if applied properly, makes writing unit tests much easier and, yes, enjoyable.
This post is not an introduction into Hamcrest though. It’s about a topic which has not been covered often.
We at dmfs aim to create a Hamcrest Matcher for every new type we create so we can write tests quickly, easily and in a declarative manner.
The one thing we didn’t test with a Hamcrest Matcher yet were our Hamcrest Matchers themselves.
The tests for our Matchers usually look old fashioned like this (example from jems
PresentMatcherTest.java)
@Test
public void testMatchesSafely() throws Exception
{
assertThat(new PresentMatcher<>().matchesSafely(absent(), new Description.NullDescription()), is(false));
assertThat(new PresentMatcher<String>().matchesSafely(new Present<>("test"), new Description.NullDescription()), is(true));
assertThat(new PresentMatcher<>("test").matchesSafely(new Present<>("test"), new Description.NullDescription()), is(true));
assertThat(new PresentMatcher<>("test").matchesSafely(new Present<>("tost"), new Description.NullDescription()), is(false));
assertThat(present().matchesSafely(new Present<>("test"), new Description.NullDescription()), is(true));
assertThat(present("test").matchesSafely(new Present<>("test"), new Description.NullDescription()), is(true));
assertThat(present(is("test")).matchesSafely(new Present<>("test"), new Description.NullDescription()), is(true));
assertThat(present("tost").matchesSafely(new Present<>("test"), new Description.NullDescription()), is(false));
assertThat(present(is("tost")).matchesSafely(new Present<>("test"), new Description.NullDescription()), is(false));
}
@Test
public void testValueMismatchDescription() throws Exception
{
Description mismatchMsg = new StringDescription();
new PresentMatcher<>("123").describeMismatch(new Present<>("abc"), mismatchMsg);
assertThat(mismatchMsg.toString(), is("present, but value was \"abc\""));
}
@Test
public void testPresenceMismatchDescription() throws Exception
{
Description mismatchMsg = new StringDescription();
new PresentMatcher<>("123").describeMismatch(absent(), mismatchMsg);
assertThat(mismatchMsg.toString(), is("not present"));
}
@Test
public void testDescribeTo() throws Exception
{
Description description = new StringDescription();
new PresentMatcher<>("123").describeTo(description);
assertThat(description, hasToString("present with value \"123\""));
}
Although these tests use assertThat
and Matchers, this is not much different from testing with the good old assertEquals
and assertTrue
and completely misses the point of Hamcrest.
MatcherMatcher
Much to my surprise the Hamcrest library doesn’t seem to contain any Matcher
to test Matchers, nor was I able to find anything like that.
That’s why, in jems
1.18, we’ve finally published Hamcrest Matcher
s to test other Hamcrest Matcher
s.
The class MatcherMatcher
contains variations of the following three types of Matcher
s.
matches
takes an object of type <T>
and checks whether the testee successfully matches this object. There is only one version of this which takes the matching object.
mismatches
takes an object of type <T>
and checks whether the testee successfully mismatches this object. There are two more versions of this matcher which also test
the resulting mismatch description. They take an additional String
(matched with is(…)
) or any other Matcher<String>
.
describesAs
matches the description of a Matcher
. It either takes an argument which is a Matcher
to match the String
value of the description or the actual String
of the description.
Usage
Putting it all together we can re-write the test for PresentMatcher
containing only three assertThat
statements.
@Test
public void test() throws Exception
{
assertThat(present(),
AllOf.<Matcher<Optional<Object>>>allOf(
matches(new Present<>(new Object())), // matches any present value
mismatches(new Absent<>(), "not present"), // mismatches absent only
describesAs("present with value ANYTHING")
)
);
assertThat(present("test"),
AllOf.<Matcher<Optional<String>>>allOf(
matches(new Present<>("test")), // value must match exactly
mismatches(new Absent<>(), "not present"), // mismatches absent …
mismatches(new Present<>("abc"), "present, but value was \"abc\""), // … and wrong values
describesAs("present with value \"test\"")
)
);
assertThat(present(is("test")),
AllOf.<Matcher<Optional<String>>>allOf(
matches(new Present<>("test")), // value must match exactly
mismatches(new Absent<>(), "not present"), // mismatches absent …
mismatches(new Present<>("abc"), "present, but value was \"abc\""), // … and wrong values
describesAs("present with value is \"test\"")
)
);
}
This is a far better test than the version above. It’s more compact and it’s declarative.
It should be mentioned the class Present
is deliberately not tested using PresentMatcher
in order to allow its usage in this test without circular reasoning.
The tests for these new Matchers use MatcherMatcher
s as well, except for those methods which would result in circular reasoning, of course.
Finally there is no excuse anymore for not having your custom Hamcrest Matchers tested properly.