Testing Exceptions with JUnit

Categories: Java

When using the JUnit test framework for Java, it is sometimes necessary to verify that code under test throws an exception given specific inputs/conditions; here are four options for writing such tests. The first three are pretty widely known; the fourth is an interesting application of Java closures.

Manually Catch Exception

This approach doesn’t need any special JUnit features, just some boilerplate code:

@Test
public void testSomething() throws Exception {
   try {
      someMethod(...);
      Assert.fail("No exception thrown");
   } catch(IllegalArgumentException e) {
      Assert.assertEquals("expected message", e.getMessage());
   }
}

Pros are:

  • obvious code
  • ability to test properties of the thrown exception
  • allows testing of an exception in the middle of a test, ie the test-method may continue.

Cons are:

  • verbose

Expected Clause in @Test Annotation

JUnit’s @Test annotation supports an “expected” attribute which defines an exception that the method is expected to throw:

@Test(expected=IllegalArgumentException.class)
public void testSomething() throws Exception {
   ...
}

Pros are:

  • built-in, widely known
  • compact/terse

Cons are:

  • no option to verify properties of the exception other than its type
  • the method throwing an exception must be the last line in the test

The ExpectedException Rule

JUnit comes with a number of standard “rule” classes; one of these is the ExpectedException rule:

public class Foo {
  @Rule
  public final ExpectedException expected = ExpectedException.none();

  @Test
  public void testSomething() throws Exception {
    expected.expect(SomeExceptionType.class);
    // optionally call other methods on object expected to define further criteria

    // call a method which throws an exception
    someMethod();
  }
}

Any class member annotated with @Rule becomes a “wrapper” for every method annotated with @Test.

The ExpectedException class:

  • clears all its “match criteria” before the test-method is invoked
  • catches any exception thrown by the test-method and reports failure if it does not match the current “match criteria” - which can be defined from within the test-method.
  • fails the test if an exception is thrown which does not match, or if no exception is thrown

Pros are:

  • built-in
  • compact/terse
  • allows testing of various exception properties

Cons are:

  • not widely known
  • behaviour is not obvious; the configuration of the expected criteria is separated from the method which throws the exception.
  • requires a class member with annotation; this is not a huge problem, but a little verbose. The object is reused for each test-method, which is not so obvious.
  • complex tests of the thrown exception require writing a Hamcrest matcher - which is not always trivial
  • the method throwing an exception must be the last line in the test
  • whether a test-method is expected to throw an exception or not is not immediately obvious; it depends on whether there are calls to the expected member object from within the test.

See the javadoc for this class for more information.

Closure-Based Helper

This solution defines a static method which takes three parameters:

  • an expected exception type
  • a closure to test
  • a closure for examining the thrown exception

The test-method invokes the code that is expected to throw an exception via a closure, eg:

@Test
public void testSomething() throws Exception {
   ...
   TestUtils.assertThrows(
      IllegalArgumentException.class,
      () -> someMethod(),
      (e) -> Assert.assertEquals("expected message", e.getMessage()));
}

Pros are:

  • fairly compact/terse
  • fairly obvious
  • allows testing of various exception properties
  • allows testing of an exception in the middle of a test, ie the test-method may continue.

Cons are:

  • not widely used
  • not built-in
  • requires java 1.8

I was inspired to create this implementation by this posting. My implementation of the concept is as follows:

// Author: Simon Kitching
// This code is in the public domain
package net.vonos.testsupport;

import org.junit.Assert;
import java.util.function.Consumer;

/**
 * Some static methods useful for unit-testing.
 * <p>
 * The "assertThrows" methods can be used in a unit-test to assert that a specific method being tested will
 * throw an exception of a specific type, with a specific message. This is an alternative to the junit
 * "@Rule ExpectedException" feature.
 * </p>
 */
public final class TestUtils {
    /**
     * Helper interface needed by the assertThrows methods.
     * <p>
     * Any closure which takes no parameters can be cast to this type.
     * </p>
     */
    @FunctionalInterface
    public interface ThrowableRunnable {
        void run() throws Throwable;
    }

    /**
     * Verify that a method throws an exception with specific values.
     * <p>
     * Example:
     * <code>
     *  TestUtils.assertThrows(SomeException.class, () -> someMethodThatThrows(...))
     * </code>
     * </p>
     *
     * @param throwable is the exception that must be thrown
     * @param runnable is a closure that invokes the method to be tested
     */
    public static <T extends Throwable> void assertThrows(Class<T> throwable, ThrowableRunnable runnable) {
        assertThrows(throwable, runnable, t -> {});
    }

    /**
     * Verify that a method throws an exception with specific values.
     * <p>
     * Example:
     * <code>
     *  TestUtils.assertThrows(SomeException.class,
     *  () -> someMethodThatThrows(...),
     *  (e) -> Assert.assertEquals("some message", e.getMessage()));
     * </code>
     * </p>
     *
     * @param throwable is the exception that must be thrown
     * @param runnable is a closure that invokes the method to be tested
     * @param postCheck is a closure that is passed the thrown exception
     */
    public static <T extends Throwable> void assertThrows(
        Class<T> throwable,
        ThrowableRunnable runnable,
        Consumer<T> postCheck) {

        try {
            runnable.run();
            Assert.fail("No exception was thrown");
        } catch (Throwable t) {
            if (!throwable.isInstance(t)) {
                Assert.fail("Unexpected exception type:" + t.getClass());
            }
            @SuppressWarnings("unchecked")
            T ex = (T) t;
            postCheck.accept(ex);
        }
    }

    /** private constructor - this is a utils class. */
    private TestUtils() {
    }
}

Conclusions

I would generally recommend @Test(expected=..) when it is sufficient, and the manual try/catch solution for other cases.

I find the ExpectedException rule insufficiently flexible, and non-obvious. IMO the closure-based helper is better than ExpectedException, but not sufficiently better than the manual try/catch solution to make it worth the effort. Nevertheless, it is interesting that such a thing can be implemented with Java closures.