JUnit: testing exception with Java 8 and Lambda Expressions
In JUnit there are many ways of testing exceptions in test code, including try-catch idiom
, JUnit @Rule
, with catch-exception
library. As of Java 8 we have another way of dealing with exceptions: with lambda expressions. In this short blog post I will demonstrate a simple example how one can utilize the power of Java 8 and lambda expressions to test exceptions in JUnit.
Note: The motivation for writing this blog post was the message published on the catch-exception
project page:
Java 8’s lambda expressions will make catch-exception redundant. Therefore, this project won’t be maintained any longer
SUT - System Under Test
We will test exceptions thrown by the below 2 classes.
The first one:
class DummyService {
public void someMethod() {
throw new RuntimeException("Runtime exception occurred");
}
public void someOtherMethod(boolean b) {
throw new RuntimeException("Runtime exception occurred",
new IllegalStateException("Illegal state"));
}
}
And the second:
class DummyService2 {
public DummyService2() throws Exception {
throw new Exception("Constructor exception occurred");
}
public DummyService2(boolean dummyParam) throws Exception {
throw new Exception("Constructor exception occurred");
}
}
Desired Syntax
My goal was to achieve syntax close to the one I had with catch-exception
library:
package com.github.kolorobot.exceptions.java8;
import org.junit.Test;
import static com.github.kolorobot.exceptions.java8.ThrowableAssertion.assertThrown;
public class Java8ExceptionsTest {
@Test
public void verifiesTypeAndMessage() {
assertThrown(new DummyService()::someMethod) // method reference
// assertions
.isInstanceOf(RuntimeException.class)
.hasMessage("Runtime exception occurred")
.hasNoCause();
}
@Test
public void verifiesCauseType() {
// lambda expression
assertThrown(() -> new DummyService().someOtherMethod(true))
// assertions
.isInstanceOf(RuntimeException.class)
.hasMessage("Runtime exception occurred")
.hasCauseInstanceOf(IllegalStateException.class);
}
@Test
public void verifiesCheckedExceptionThrownByDefaultConstructor() {
// constructor reference
assertThrown(DummyService2::new)
// assertions
.isInstanceOf(Exception.class)
.hasMessage("Constructor exception occurred");
}
@Test
public void verifiesCheckedExceptionThrownConstructor() {
// lambda expression
assertThrown(() -> new DummyService2(true))
// assertions
.isInstanceOf(Exception.class)
.hasMessage("Constructor exception occurred");
}
@Test(expected = ExceptionNotThrownAssertionError.class) // making test pass
public void failsWhenNoExceptionIsThrown() {
// expected exception not thrown
assertThrown(() -> System.out.println());
}
}
Note: The advantage over catch-exception
is that we will be able to test constructors that throw exceptions.
Creating the ‘library’
Syntatic sugar
assertThrown
is a static factory method creating a new instance of ThrowableAssertion
with a reference to caught exception.
package com.github.kolorobot.exceptions.java8;
public class ThrowableAssertion {
public static ThrowableAssertion assertThrown(ExceptionThrower exceptionThrower) {
try {
exceptionThrower.throwException();
} catch (Throwable caught) {
return new ThrowableAssertion(caught);
}
throw new ExceptionNotThrownAssertionError();
}
// other methods omitted for now
}
The ExceptionThrower
is a @FunctionalInterface
which instances can be created with lambda expressions, method references, or constructor references. assertThrown
accepting ExceptionThrower
will expect and be ready to handle an exception.
@FunctionalInterface
public interface ExceptionThrower {
void throwException() throws Throwable;
}
Assertions
To finish up, we need to create some assertions so we can verify our expactions in test code regarding teste exceptions. In fact, ThrowableAssertion
is a kind of custom assertion providing us a way to fluently verify the caught exception. In the below code I used Hamcrest
matchers to create assertions. The full source of ThrowableAssertion
class:
package com.github.kolorobot.exceptions.java8;
import org.hamcrest.Matchers;
import org.junit.Assert;
public class ThrowableAssertion {
public static ThrowableAssertion assertThrown(ExceptionThrower exceptionThrower) {
try {
exceptionThrower.throwException();
} catch (Throwable caught) {
return new ThrowableAssertion(caught);
}
throw new ExceptionNotThrownAssertionError();
}
private final Throwable caught;
public ThrowableAssertion(Throwable caught) {
this.caught = caught;
}
public ThrowableAssertion isInstanceOf(Class<? extends Throwable> exceptionClass) {
Assert.assertThat(caught, Matchers.isA((Class<Throwable>) exceptionClass));
return this;
}
public ThrowableAssertion hasMessage(String expectedMessage) {
Assert.assertThat(caught.getMessage(), Matchers.equalTo(expectedMessage));
return this;
}
public ThrowableAssertion hasNoCause() {
Assert.assertThat(caught.getCause(), Matchers.nullValue());
return this;
}
public ThrowableAssertion hasCauseInstanceOf(Class<? extends Throwable> exceptionClass) {
Assert.assertThat(caught.getCause(), Matchers.notNullValue());
Assert.assertThat(caught.getCause(), Matchers.isA((Class<Throwable>) exceptionClass));
return this;
}
}
AssertJ Implementation
In case you use AssertJ
library, you can easily create AssertJ
version of ThrowableAssertion
utilizing org.assertj.core.api.ThrowableAssert
that provides many useful assertions out-of-the-box. The implementation of that class is even simpler than with Hamcrest
presented above.
package com.github.kolorobot.exceptions.java8;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowableAssert;
public class AssertJThrowableAssert {
public static ThrowableAssert assertThrown(ExceptionThrower exceptionThrower) {
try {
exceptionThrower.throwException();
} catch (Throwable throwable) {
return Assertions.assertThat(throwable);
}
throw new ExceptionNotThrownAssertionError();
}
}
An example test with AssertJ
:
public class AssertJJava8ExceptionsTest {
@Test
public void verifiesTypeAndMessage() {
assertThrown(new DummyService()::someMethod)
.isInstanceOf(RuntimeException.class)
.hasMessage("Runtime exception occurred")
.hasMessageStartingWith("Runtime")
.hasMessageEndingWith("occurred")
.hasMessageContaining("exception")
.hasNoCause();
}
}
Update - AAA Style
Frank Appel in his Clean JUnit Throwable-Tests with Java 8 Lambdas post suggests to distinguishact
and assert
phases of the test for the clear visual separation (improving readability). The idea is to return the Throwable
(instead of ThrowableAssert
) in an act
phase of the test and then pass it for assertion in the assert
phase, so the test will look like the below:
@Test
public void aaaStyle() {
// arrange
DummyService dummyService = new DummyService();
// act
Throwable throwable = ThrowableCaptor.captureThrowable(dummyService::someMethod);
// assert
assertThat(throwable)
.isNotNull()
.hasMessage("Runtime exception occurred");
}
Where ThrowableCaptor
is defined as follows:
public class ThrowableCaptor {
public static Throwable captureThrowable(ExceptionThrower exceptionThrower) {
try {
exceptionThrower.throwException();
// not exception was thrown
return null;
} catch (Throwable caught) {
return caught;
}
}
}
Summary
With just couple of lines of code, we built quite cool code helping us in testing exceptions in JUnit without any additional library. And this was just a start. Harness the power of Java 8 and lambda expressions!
References
- Source code for this article is available on GitHub (have a look at
com.github.kolorobot.exceptions.java8
package) - Some other articles of mine about testing exceptions in JUnit. Please have a look:
If you like my articles, please subscribe to http://blog.codeleak.pl here: http://feeds.feedburner.com/codeleak or follow me on twitter: https://twitter.com/kolorobot. I usually blog and tweet about Java and Spring.
Hi Rafał,
ReplyDeletethere is missing boolean parameter in the signature of the method "someOtherMethod",
great article!
Thanks!
DeleteI updated the code.
Hello Rafal,
ReplyDeleteWhere is the source code of ExceptionNotThrownAssertionError ?
Hello,
DeleteAll source code can be found on GitHub. See: https://github.com/kolorobot/unit-testing-demo/tree/master/src/test/java/com/github/kolorobot/exceptions/java8