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
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, 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.
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.
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() requires two arguments,
Class <T> and
assertThrows() can also take an optional third argument of either
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.
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.