Saturday, October 24, 2009

Testing Exception Messages, JUnit 4.7 Edition

Historically, JUnit has not elegantly supported verification of expected-exception messages in unit tests. Some time ago, I wrote a post about techniques for handling this situation. Brian Brooks (clearly a diligent web searcher) found that post and was kind enough to send me a message pointing out that the latest version of JUnit, 4.7, finally includes support for this incredibly important and common testing scenario.

In honor of this exciting occasion, I wanted to post an example. Here is the class I'm going to test:
public class PositiveInteger {
 private int num;

 public PositiveInteger(int num) {
  if(num <= 0) {
   throw new IllegalArgumentException("Number must be positive");
  }
  this.num = num;
 }
}
Let's test -- hmmm -- the constructor, I guess. I created two test cases -- one which passes, and one which intentionally fails (just to show the resulting output):
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;


public class TestPositiveNumber {

 @Rule
 public ExpectedException thrown = ExpectedException.none();

 @Test
 public void testConstructorInvalid_PASS() {
  thrown.expect(IllegalArgumentException.class);
  thrown.expectMessage("Number must be positive");
  new PositiveInteger(-5);
 }

 /**
  * Intentionally-failing test to demonstrate the results
  * when checking an exception message.
  */
 @Test
 public void testConstructorInvalid_FAIL() {
  thrown.expect(IllegalArgumentException.class);
  thrown.expectMessage("Not gonna pass");
  new PositiveInteger(-5);
 }
}
JUnit now supports a style embodied in "rules", which get marked with the @Rule annotation and provide a means by which the test execution behavior can be modified. You can see that here with the "thrown" field, of type "ExpectedException". It would be great if a) that field didn't have to be public, and b) the functionality could be implemented using a local variable, but unfortunately the Java language won't currently support such an implementation. So a public field it is. In a test, you can configure the rule. Here I do that with calls to the "expect" and "expectMessage" methods. When JUnit executes the test case, it checks the result against the rule and fails if the rule is not matched. In the test case that does not pass, the failure takes this form:
java.lang.AssertionError: 
Expected: (exception with message a string containing "Not gonna pass" and an instance of java.lang.IllegalArgumentException)
     got: 

 at org.junit.Assert.assertThat(Assert.java:778)
 at org.junit.Assert.assertThat(Assert.java:736)
Very useful. Thanks to Kent Beck for adding this support.

No comments: