Saturday 7 August 2010

Hamcrest 1.1, AllOf, DSL, SuppressWarnings And Maven Central

Hi,

I've never used Hamcrest before; today is my first day with it. And I have already had a lot of "fun" which I would like to share.

My experiences today have been centred around the AllOf matcher. Or rather around this version shipped with Hamcrest 1.1. This is the newest version you can find on Maven central. It also seems to be built into the newest JUnit jar version 4.8.1.

Imagine that in your test you've got variable a of type A and you want to check it against three matchers m1, m2 and m3 of type Matcher<A>. It was quite natural for me to write it like this:

import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertThat;
...

@Test
public void testA() {
    ...
    assertThat(a, allOf(m1, m2, m3)); // here you get a warning
}

That works and looks nice. But you get a compiler warning: Type safety : A generic array of Matcher is created for a varargs parameter. I have found this blog post to be very helpful in explaining the nature of this warning. The quickest solution is to write:

@Test
@SuppressWarnings("unchecked")
public void testA() {
    ...
    assertThat(a, allOf(m1, m2, m3));
}

However it would be nice if we could preserve full type checking in the test code. It took me a while to find an alternative solution:

@Test
public void testA() {
    ...
    List<Matcher<? extends A>> matchers
            = new LinkedList<Matcher<? extends A>>();
    matchers.add(m1);
    matchers.add(m2);
    matchers.add(m3);

    assertThat(a, allOf(matchers));
}

This is fully type checked. Unfortunately it is a bit more long-winded and we looses the DSL feeling. On a side note this code can not be shortened to just:

@Test
public void testA() {
    ...
    // doesn't compile
    List<Matcher<? extends A>> matchers = Arrays.asList(m1, m2, m3);

    assertThat(a, allOf(matchers));
}

This sample deceptively looks as legit as the one above it. However it fails to compile because of a Type mismatch: cannot convert from List<Matcher<A>> to List<Matcher<? extends A>>.

I've had to discovere all this the hard way because in Hamcrest 1.1 AllOf.allOf is defined as:

public static Matcher allOf(
        Matcher<? extends T>... matchers) {
    return allOf(Arrays.asList(matchers));
}

public static Matcher allOf(
        Iterable<Matcher<? extends T>> matchers) {
    return new AllOf(matchers);
}


My day would have been saved if I had switched to Hamcrest 1.2. Nat Pryce has fixed the issue in revision 258. We get explicit overloads of allOf method for up to 6 matchers in one go. This makes my very first code sample compile and run without any warnings.

It is also interesting to note that shortly before this commit another issue had been corrected in Hamcrest codebase: revision 194. Matcher<? extends A> has been replaced with Matcher<? super A>. This is most reasonable: indeed we can apply a Matcher<Object> to test a String. On the other hand it is not very useful to apply a Matcher<SomeClassExtendingA> when our object is known to be of class A.

1.2 release of Hamcrest contains both fixes. Unfortunately it is only available on the project website but not on Maven central. Having spent quite a bit of time on the investigation what do I do now? I see three options:
  • use the long-winded version of code
  • wait for Hamcrest 1.2 to be uploaded to Maven central
  • copy-paste AllOf into my own code base and backport these two fixes

I have chosen the last option. This is my temporary fix. It makes my test code look nice right now and I can easily take out my hack later when a newer Hamcrest appears on Maven central.

Speaking of which: to use an updated version of Hamcrest I will need a version of JUnit which doesn't package hamcrest classes inside its own jar. Such a version exists, it is called junit-dep. Unfortunately the latest version Maven central currently has is only 4.5. Does anybody know of any way to cause a newer version of junit-dep to be uploaded? As far as I can tell there is just one person behind JUnit, Kent Beck. Is this right? Is there any chance to accomplish this with or without his help?

No comments: