Monday, January 5, 2015

Exceptional JUnit Rules

I recently got aggravated at the JUnit capability for handling and testing expected exceptions.  Usually it looks like this:

@Test(expected = IllegalArgumentException.class)
public void testBadConditions()
{
}

That works just fine when first assembled, but what happens when that same exception class is thrown by a different piece of code, for a different reason?  The answer, the test passes.  This hit me when I was implementing a builder pattern in my etl-unit framework using a TDD approach, and my tests for validating parameters would pass, but when I added more validation the same exceptions were being thrown for different reasons (because I altered the order of validation) so the tests were still passing, but parts of code were being hidden from test cases and were no longer being covered.  When using TDD you tend to lean heavily on the unit tests, and things like this can be a big problem (tests passing when they should not).  I like to know right away when I have broken something.

I had a couple of options that I knew about:


  1. I could use a different exception class (or subclass) for each validation rule.
  2. I could use try/catch blocks in my tests and manually check the exception class and message.
Option (1) sucked, and in some cases I couldn't do it anyway, so I ruled that right out.  The second would allow me to use distinct messages to make them verifiable, but made for ugly tests (my opinion):

@Test
public void testBadConditions()
{
  try
  {
    causeAnError();
    // in case an exception is not thrown
    Assert.fail();
  }
  catch(IllegalArgumentException exc)
  {
     Assert.assertEquals("expected message", exc.getMessage());
  }
}

I didn't care too much for that - way too much noise in my tests.  I initially googled it and found, as expected, a bunch of irritating, condescending posts about why this is always bad and JUnit would never implement this.  The @Test annotation indeed does not support adding a message, but I have been using JUnit rules a bit recently and I hoped there might be a rule for this.  Suspecting that it would be called ExpectedException, I googled for that.  To my delight, I found it.  Here is an example of the above test using this rule:

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

@Test
public void testBadConditions()
{
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("expected message");

    causeAnError();
}

@Test
public void noExceptionThrownTest()
{
 ...
}

I really like the way this looks.  It is functionally equivalent to the previous example.  The declaration of the @Rule is initialized with ExpectedException.none() so that the default state before each test is not to expect an exception, and noExceptionThrownTest does not expect an exception as usual.  Then, at the start of the test, as with other JUnit rules, you tell the rule to expect an exception class and then give it the message to expect.

Other things you can do with this rule are specify a regular expression for the message, and assert an exception cause class type.

I'm not sure I like this better than having it in the @Test annotation, but I do like it way better than my alternatives.

No comments:

Post a Comment