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.