Flexible JUnit assertions with assertThat()
Update: This got a lot of attention, and lead to Nat and I extracting the constraint library out from jMock, and creating Hamcrest.
Note: When this post was w
Over time I’ve found I end up with a
gazillion permutation of assertion methods
in JUnit: assertEquals
,
assertNotEquals
,
assertStringContains
,
assertArraysEqual
,
assertInRange
,
assertIn
, etc.
Here’s a nicer way.
jMock contains a constraint library for specifying precise expectations on mocks that can be reused in your own assertion method (and that’s the last time I’m going to mention mocks today, I promise – despite the frequent references to the jMock library).
By making a simple JUnit assertion method that takes a Constraint, it provides a replacement for all the other assert methods.
I call mine
assertThat()
because I think it reads well. Combined with
the jMock syntactic sugar, you can use it
like this:
assertThat(something, eq("Hello"));
assertThat(something, eq(true));
assertThat(something, isA(Color.class));
assertThat(something, contains("World"));
assertThat(something, same(Food.CHEESE));
assertThat(something, NULL);
assertThat(something, NOT_NULL);
Okay, that’s nice but nothing radical. A bunch of assert methods have been replaced with different methods that return constraint objects. But there’s more…
Composing constraints
Constraints can be chained making it
possible to compose them in different
permutations. For instance, for virtually
every assertion I do, I usually find that I
need to test the negative
equivalent at some point. That’s easy,
I can use the not()
constraint:
assertThat(something, not(eq("Hello")));
assertThat(something, not(contains("Cheese")));
Or maybe a combinations of assertions,
with the or()
constraint:
assertThat(something, or(contains("color"), contains("colour")));
Improved failure messages
Of course, the previous example could also be written using the vanilla JUnit assert methods like this:
assertTrue(something.indexOf("color") > -1 || something.indexOf("colour") > -1);
When it fails, you’ll get an error message, but because the expression only evaluates to a boolean, so the failure message is not too helpful:
junit.framework.AssertionFailedError:
JUnit is not able to explain which part of the expression failed. This is awkward to debug, often involving re-running tests through a debugger or by adding additional logging.
The assertThat() approach gives a useful failure message, for free:
assertThat(something, or(contains("color"), contains("colour")));
Fails with:
junit.framework.AssertionFailedError:
Expected: (a string containing "color" or a string containing "colour")
but got : cooler
Muuuuch more helpful, right?
How to implement assertThat()
The simplest way is to include jMock and
create your own base test class that extends
MockObjectTestCase
. This brings
in convenience
methods for free (I’m still not talking
about mocks, honest). If you don’t want to
extend this class, you can easily
reimplement these methods yourself – it’s no
biggie.
import org.jmock.MockObjectTestCase;
import org.jmock.core.Constraint;
public abstract class AssertThatTestCase extends MockObjectTestCase {
protected void assertThat(Object something, Constraint matcher) {
if (!matches.eval(something)) {
StringBuffer message = new StringBuffer("\nExpected: ");
.describeTo(message);
matcher.append("\nbut got : ")
message.append(something)
.append('\n');
fail(message.toString());
}
}
}
Now ensure all your test cases extend
AssertThatTestCase
instead of
junit.framework.TestCase
and
you’re done.
Defining custom constraints
Creating new constraints is easy. Let’s say I want something like:
assertThat(something, between(10, 20));
To do that I need to create a method that
returns a Constraint
object,
requiring two methods: - eval()
for performing the actual assertion -
describeTo()
for the self
describing error message.
public Constraint between(final int min, final int max) {
return new Constraint() {
public boolean eval(Object object) {
if (!object instanceof Integer) {
return false;
}
int value = ((Integer)object).intValue();
return value > min && value < max;
}
public StringBuffer describeTo(StringBuffer buffer) {
return buffer
.append("an int between ")
.append(min)
.append(" and ")
.append(max);
}
}
}
This can be combined with other constraints and still generate decent failure messages.
assertThat(something, or(eq(50), between(10, 20));
Fails with:
junit.framework.AssertionFailedError:
Expected: (50 or an int between 10 and 20)
but got : 43
In practice I find I only need to create a few of these constraints as the different combinations gives me nearly everything I need.
More about this in the jMock documentation.
Summary
Since using this one assert method I’ve found my tests to be much easier to understand because of lack of noise and I’ve spent a lot less time creating “yet another assertion” method for specific cases.
And in most cases I never need to write a custom failure message as the failures are self describing.
Updates
- The matchers from jMock have been pulled out into a new project, Hamcrest.
- A follow up to this post shows some creative uses of matchers, and talks a bit about when you shouldn’t use them.
- JUnit 4.4 now comes with assertThat()!