assertThat was based on the Matcher interface from hamcrest, and led to more readable code, more readable error messages, and custom, combinable assertions. However, after two years of use, I’ve seen several situations in which the Matcher-based assertThat API can fail developers:
Custom matchers can be too much code for simple ideas. Ideally, custom assertions should just flow off the fingers, but I find it very telling that the JUnit self tests only have four custom matchers, all in the ResultMatchers class. Most of them are about 7 lines of mostly-boilerplate code, such as this:
Matchers can be difficult to discover. They are generally returned by static factory methods--if your IDE isn’t set up to auto-complete these methods, it can be difficult to track down whether a matcher method exists for the assertion you want, or which of several available has the right meaning. Ideally, we could restrict the types of Matchers that could type-check for a given value, to give your IDE hints. Unfortunately...
I have discovered a long, painful proof that it is impossible to provide strong types for matcher-based assertThat using:
- Java generics...
- as implemented in all of the compilers we currently support...
- without breaking the use of Matchers in jMock
This blog post is too narrow to contain it.
- Similar to the above, introducing a dependency from JUnit on hamcrest, the matcher library, causes problems for any users using a different version of hamcrest for another use (for example, jMock)
Based on these difficulties, I propose a new API for assertions, which I call dotmesh*. The idea is to use imposterization to generate assertions based on every boolean method available on every class. The new form of the assertion above would be:
If this fails, the error message is:
Failed: <ERROR: blah blah blah>.contains(<OK>)
Here’s some more examples of dotmesh in action:
Advantages of the dotmesh approach over the Matcher approach:
No additional code: assertions and error messages are automatically generated from any method that returns boolean.
Super-easy discovery: know a boolean method? You know a dotmesh assertion.
Strong typing for free: since we’re using the same methods and types, assertions only compile if they make sense.
As a bonus, dotmesh provides a reward for interfaces that follow the “Suggestion of Demeter”, and provide accessors returning booleans or collections for essential information.
An ambivalent point:
Both dotmesh and hamcrest must use specially-named methods to “talk about” operations that would otherwise be more clearly stated with operators. For example, you can easily:
assertTrue(x > 5)
but in order to find out why this assertion fails, you must use:
assertThat(x, greaterThan(5))in hamcrest, or
Disadvantages of dotmesh:
The standard imposterization tricks break down when the target being imposterized is a primitive type or final class. This isn’t too big a problem for primitive types and Strings: we can introduce a wrapper interface for each, and then be done. The more disconcerting issue is third-party final classes. We can’t generate new synthetic classes that replicate their interface, so this is going to fail at some point:
My approach for the moment is to be very apologetic in the error message. Once I see where this problem tends to come up for users, one of several possible mitigation tricks may prove possible.
The hamcrest assertThat allowed (to a certain extent) for combinations of matchers:
dotmesh can’t match that directly, but there’s often useful workarounds:
In order to pull off imposterization, I’m using objenesis and cglib, which might conceivably lead to the same versioning issues currently imposed by hamcrest. However, this dependency is completely encapsulated from the user, so we could use a solution like jarjar to avoid any conflicts.
If I’ve piqued your interest enough to try some truly bleeding-edge, no-guarantees code, check out:
* Why "dotmesh"? Well, it creates assertions by "meshing" a new meaning into the "dot" that indicates a method invocation. There's another reason, which is a cryptic homage to hamcrest,..