Handling and Verifying Exceptions in JUnit 5

JUnit 5 offers a number of improvements over JUnit 4. In this article we will take a quick look at how exceptions are handled and verified in JUnit 4, and then see how the new assertThrows() in JUnit 5 improves the usability and readability when catching and verifying exceptions.

Handling and Verifying Exceptions in JUnit 4

In JUnit 4 there are two primary ways of handling exceptions. The most commonly used method is with the expected field in @Test. An alternative way of handling exceptions is by using a @Rule and ExpectedException. Below are examples of both: 


public class TestHandleExceptionsJUnit4 {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test(expected = SpecificException.class)
public void testSpecificExceptionHandling() throws SpecificException {
onlyThrowsExceptions();
}
@Test(expected = Exception.class)//Passes because Exception is super type of SpecificException
public void testExceptionHandling() throws SpecificException {
onlyThrowsExceptions();
}
@Test(expected = SpecificException.class)
public void testExceptionHandlingVerifyExceptionFields() throws SpecificException {
try {
onlyThrowsExceptions();
} catch (SpecificException e) {
assertEquals("An exception was thrown!", e.getMessage());
throw e;
}
}
@Test
public void testUseExpectedException() throws SpecificException {
expectedException.expect(SpecificException.class);
expectedException.expectMessage("An exception was thrown!");
onlyThrowsExceptions();
}
@Test
public void testUseExpectedExceptionWithSuperType() throws SpecificException {
expectedException.expect(Exception.class);//Passes because Exception is super type of SpecificException
expectedException.expectMessage("An exception was thrown!");
onlyThrowsExceptions();
}
public void onlyThrowsExceptions() throws SpecificException {
throw new SpecificException("An exception was thrown!");
}
public class SpecificException extends Exception {
public SpecificException(String message) {
super(message);
}
}
}

While both methods are capable of catching and verifying exceptions, each have issues that impact their usability and readability. Let’s step through some of these issues with expected and ExpectedException.

When using expected,  not only are you putting some of the assertion behavior into the definition of the test case, verifying fields within the thrown exception is a bit clunky. To verify the fields of an exception you’d have to add a try/catch within the test case, and within the catch block perform the additional assertions and then throw the caught exception.

When using ExpectedException you have to initially declare it with ​none(), no exception expected, which is a bit confusing. Within a test case you define the expected behavior before the method under test. This would be similar to if you were using a mock, but it’s not intuitive as a thrown exception is a “returned” value, not a dependency nor internal to the code under test.

These oddities significantly impacted the usability and readability of test cases in JUnit 4 that verified exception behavior. The latter is by no means a trivial problem as “easy to read” is probably one of, if not the, most import characteristics of test code. So it is not surprising then that exception handling behavior was heavily rewritten in JUnit 5.

Introducing assertThrows()

In JUnit 5, the above two methods of handling and verifying exceptions have been rolled into the much more straightforward and easier to use assertThrows(). assertThrows() requires two arguments, Class <T> and Executable, assertThrows() can also take an optional third argument of either String or Supplier<String> which can be used for providing a custom error message if the assertion fails. assertThrows() returns the thrown exception, which allows for further inspection and verification of the fields within the thrown exception.

Below is an example of assertThrows() in action:


public class TestHandleExceptionsJUnit5 {
@Test
public void testExceptionHandling() {
Exception e = assertThrows(SpecificException.class, () -> onlyThrowsExceptions());
assertEquals("An exception was thrown!", e.getMessage());
}
@Test
public void testExceptionHandlingFailWrongExceptionType() {
assertThrows(Exception.class, () -> doesntThrowExceptions(), "Wrong exception type thrown!");
}
@Test
public void testExceptionHandlingFailNoExceptionThrown() {
assertThrows(SpecificException.class, () -> doesntThrowExceptions(), "An exception wasn't thrown!");
}
public void onlyThrowsExceptions() throws SpecificException {
throw new SpecificException("An exception was thrown!");
}
public void doesntThrowExceptions() {
//do nothing
}
public class SpecificException extends Exception{
public SpecificException(String message) {
super(message);
}
}
}

As can be seen in the above, assertThrows()  is much cleaner and easier to use than either method in JUnit 4. Let’s take a bit closer look at assertThrows() and some of its  more subtle improvements as well.

The second argument, the Executable is where the requirement of Java 8 in JUnit 5 starts to show its benefits. Executable is a functional interface, which allows for, with the use of a lambda, directly executing the code under test within the declaration of assertThrows(). This makes it not only easier to check for if an exception thrown, but also allows assertThrows() to return the thrown exception so additional verification can be done.

Conclusion

assertThrows() offers significant improvements to usability and readability when verifying exception behavior for code under test. This is consistent with many of the changes made in JUnit 5, which have made the writing and reading of tests easier. If you haven’t yet made the switch to JUnit 5, I hope this seeing the improvements in exception handling and verification helps to build the case for making the switch.

The code used in this article can be found here: https://github.com/wkorando/junit-5-simple-demonstrator.

EDIT: An earlier version of this blog said that assertThrows()​ doesn’t support exception subtypes, that is incorrect.

3 thoughts on “Handling and Verifying Exceptions in JUnit 5

  1. assertThrows() also supports subtypes of exceptions. For example, the following passes, since Exception is a subtype of Throwable.

    @Test
    void test() {
    Throwable throwable = assertThrows(Throwable.class, () -> {
    throw new Exception();
    });
    assertNotNull(throwable);
    }

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s