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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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);
}
LikeLike
Thanks Sam, I updated the blog post to reflect this.
LikeLike