Monday, September 10, 2012

Hamcrest 101

In the beginning was the plain assert statement, which simply checked that a condition was true.
import org.junit.Test;

import static junit.framework.Assert.assertTrue;

public class MyTest {

 @Test
 public void testFirstGeneration( ){
  assertTrue(true == false);
 }
}
But the resulting error message was not good.

junit.framework.AssertionFailedError

So the assert method was overloaded to allow customized error messages to be passed in.
import org.junit.Test;

import static junit.framework.Assert.assertTrue;

public class MyTest {

 @Test
 public void testFirstGeneration( ){
  assertTrue("true does not equal false", true == false);
 }
}
This is not an ideal solution, however, as the error message is hard coded into the assert statement.  The next generation of assert statements solved this problem by generating the proper error message for you.
import org.junit.Test;

import static junit.framework.Assert.assertEquals;

public class MyTest {

 @Test
 public void testSecondGeneration( ){
  assertEquals(true, false);
 }
}
This only presents us with another challenge.  Just by looking at the above assert statement, can you tell which is the expected value and which is the actual value?  Of course the answer is easy if you look at the API or the resulting error message, but that wastes too much time.

java.lang.AssertionError: expected:<true> but was:<false>

And now without further ado let me introduce you to the wonderful world of Hamcrest.
import static org.junit.Assert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;

import org.junit.Test;

public class MyTest {

 @Test
 public void testThirdGeneration( ){
  assertThat(true, is(equalTo(false)));
 } 
}
Notice that, except for the parenthesis, this code reads the way it behaves.  We're asserting that the actual value is equal to the expected value.  The resulting error message confirms this:

java.lang.AssertionError:
  Expected: is <false>
    got: <true>

Creating Custom Matchers

Suppose you wanted to check if an integer value is the average of an array of integers.

The following shows one way to accomplish this:
import static org.junit.Assert.fail;

public class AssertAverage {

 public static void assertAverageOf(int actual, int[] numbers {
  int sum = 0;

  for(int num:numbers){
   sum = sum + num;
  }

  int average = (sum/numbers.length);

  if(actual != average){
   StringBuilder stringBuilder = new StringBuilder( );
   
   stringBuilder.append("expected: average of <");
   stringBuilder.append(average);
   stringBuilder.append("> but was <");
   stringBuilder.append(actual);
   stringBuilder.append(">");
   
   fail(stringBuilder.toString( ));
  }
 }
}
And here's how it would look in a unit test:
import org.junit.Test;
import static  MyPackage.AssertAverage.assertAverageOf;

public class MyClass {
 
 @Test
 public void testAssertAverage_good( ){
  assertAverageOf(5, new int[] {1,2,3,4,5,6,7,8,9});
 }

 @Test
 public void testAssertAverage_bad( ){
  assertAverageOf(0, new int[] {1,2,3,4,5,6,7,8,9});
 }
}
Now let's see what our custom assert method would look like in Hamcrest.  assertThat expects a Matcher type.  Matcher is an interface, but according to the JavaDoc we are told not to implement it.  Instead we are instructed to extend from BaseMatcher.  Unfortunately, BaseMatcher is a Generic type and we only want to accept Integers.  Therefore we need an upper bound on the type.
public class AverageOf<I extends Integer> extends BaseMatcher<I> {
Because BaseMatcher is an abstract class, we are forced to implement two methods: matches and describeTo.  matches is where the actual testing takes place. The describeTo method details what gets displayed on the expected line.  Here's how they look for our custom class:
 public boolean matches(Object obj) {
  if(obj instanceof Integer){
   int expected = ((Integer)obj).intValue();
   return expected == average;    
  }
  
  return false;  
 }

 public void describeTo(Description description) {
  description.appendText("average of " + average);
 }   
The only thing left now is to implement the averageOf method.
 private int average;

 public static <I extends Integer> Matcher<I> averageOf(I[] numbers) {
  int sum = 0;
  
  for(int number:numbers){
   sum = sum + number;
  }
 
  Matcher matcher = new AverageOf( );
  
  ((AverageOf)matcher).average = sum/numbers.length;
  
  return matcher;
 }
Most of the examples that I've looked at include both the method and a constructor.  In my opinion, this is unnecessary as it just clutters up the resulting assert statement.  Consider how messy the following looks...
Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
assertThat(5, is(new AverageOf<Integer>(numbers)));
...when compared to...
Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
assertThat(5, is(averageOf(numbers));
This article was inspired by the Hamcrest entry on Wikipedia.

No comments:

Post a Comment