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:

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:

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.