Dienstag, 13. Januar 2015

Extending JUnit with readable Boolean Assertions

Hamcrest

I like my tests to be elegant and self-reading. Thus I like Hamcrest, of course. For those of you, who don't know Hamcrest: It's an addition to JUnit that makes your code to be read like a sentence:

List list = Arrays.asList("foo", "bar");
assertThat(list, is(not(empty())));
assertThat(list.get(0), is("foo"));
assertThat(list, hasItem("bar"));

The advantage of such test are not only that they can be read like sentences, but also that they produce self-explaining error messages without writing one character of description. I.e. when you change the last line of the example to

assertThat(list, hasItem("baz"));
the resulting error message is 'Expected a collection containing "baz", but was ["foo", "bar"]' which explains pretty clearly what the problem is.

This does not only work for collections, but also for your own objects and their properties:

assertThat(myObject.getBar(), is(nullValue()));
which would lead to 'Expected is null, but was "bar"' when you return "bar" instead of the expected null. Even in this case you can guess the problem from the error message, even when it doesn't work as well as in the collection case.

Error messages become totally useless, when it comes to boolean assertions. I.e.

assertThat(myObject.isBar(), is(true));
would lead to 'Expected is <true>, but was <false>' and you don't have a clue of what went wrong. You can improve the error messages by using the 'hasProperty' matcher like
assertThat(myObject, hasProperty("bar", is(true)));
which leads to 'Expected hasProperty("bar", is <true>) but property 'bar' was <false>". The message is somewhat readable, but the code to get there is horrible, because it is not really readable any more plus we totally lose type-safety which sooner or later would lead to failing tests due to refactoring.

Boolean Assertions

Having this problem, I came up with an idea how to extend JUnit to support self-explaining boolean assertions. With my tiny extension the above code looks like
assertThat(myObject).isBar();
and, when bar returns false, the error message reads like: 'Expected result of method isBar expected <true> but was <false>'. The other way round would look like
assertThatNot(myObject).isBar();
With this extension I get readable code and explaining error messages. How did I achieve this? My tiny little JUnit extension is written with the help of cglib (2.1_3) and fits in one class:
import static org.junit.Assert.assertEquals;

import java.lang.reflect.Method;

import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.InvocationHandler;

public class Assert {

  public static  void assertThat(T actual,
                                    Matcher matcher) {
    MatcherAssert.assertThat(actual, matcher);
  }

  public static  T assertThat(T object) {
    return assertThat(object, true);
  }

  public static  T assertThatNot(T object) {
    return assertThat(object, false);
  }

  private static  T assertThat(final T object,
                                  final boolean value) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(object.getClass());
    enhancer.setCallback(new InvocationHandler() {
      @Override
      public Object invoke(Object proxy,
                           Method method,
                           Object[] args) throws Throwable {
        if (method.getReturnType() != Boolean.TYPE) {
           throw new IllegalStateException(
             "Wrong usage of assertThat for method "
             + method.getName()
             + ". Please use with boolean methods only");
        }
        boolean result
          = (Boolean)method.invoke(object, args);
        assertEquals("result of method "
          + method.getName(), value, result);
        return result;
      }
    });
    return (T) enhancer.create();
  }
}
The first method just delegates to Hamcrest's assertThat and is needed just to support static imports of 'classic' assertThat in JUnit-Tests. I hope this post helps you to write elegant and self-reading tests.