I know that one can define an 'expected' exception in JUnit, doing:
@Test(expect=MyException.class)
public void someMethod() { ... }
But what if there is always same exception thrown, but with different 'nested' causes.
Any suggestions?
I know that one can define an 'expected' exception in JUnit, doing:
@Test(expect=MyException.class)
public void someMethod() { ... }
But what if there is always same exception thrown, but with different 'nested' causes.
Any suggestions?
You could wrap the testing code in a try / catch block, catch the thrown exception, check the internal cause, log / assert / whatever, and then rethrow the exception (if desired).
As of JUnit 4.11 you can use the ExpectedException
rule's expectCause()
method:
import static org.hamcrest.CoreMatchers.*;
// ...
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void throwsNestedException() throws Exception {
expectedException.expectCause(isA(SomeNestedException.class));
throw new ParentException("foo", new SomeNestedException("bar"));
}
expectedException.expectCause(is(IsInstanceOf.<Throwable>instanceOf(SomeNestedException.class)));
but other than that, it's an elegant solution. –
Antidisestablishmentarianism expectedException.expectCause(IsInstanceOf.<Throwable>instanceOf(SomeNestedException.class));
–
Leopard isA
as a shorthand for is(instanceOf(clazz))
, so this would suffice: expectedException.expectCause(isA(SomeNestedException.class));
–
Aldo org.hamcrest.CoreMatchers.isA
. Not in Matchers
class. –
Mallon Assert.assertThrows
. –
Paganism You could wrap the testing code in a try / catch block, catch the thrown exception, check the internal cause, log / assert / whatever, and then rethrow the exception (if desired).
If you're using the latest version of JUnit you can extend the default test runner to handle this for you (without having to wrap each of your methods in a try/catch block)
ExtendedTestRunner.java - New test runner:
public class ExtendedTestRunner extends BlockJUnit4ClassRunner
{
public ExtendedTestRunner( Class<?> clazz )
throws InitializationError
{
super( clazz );
}
@Override
protected Statement possiblyExpectingExceptions( FrameworkMethod method,
Object test,
Statement next )
{
ExtendedTest annotation = method.getAnnotation( ExtendedTest.class );
return expectsCauseException( annotation ) ?
new ExpectCauseException( next, getExpectedCauseException( annotation ) ) :
super.possiblyExpectingExceptions( method, test, next );
}
@Override
protected List<FrameworkMethod> computeTestMethods()
{
Set<FrameworkMethod> testMethods = new HashSet<FrameworkMethod>( super.computeTestMethods() );
testMethods.addAll( getTestClass().getAnnotatedMethods( ExtendedTest.class ) );
return testMethods;
}
@Override
protected void validateTestMethods( List<Throwable> errors )
{
super.validateTestMethods( errors );
validatePublicVoidNoArgMethods( ExtendedTest.class, false, errors );
}
private Class<? extends Throwable> getExpectedCauseException( ExtendedTest annotation )
{
if (annotation == null || annotation.expectedCause() == ExtendedTest.None.class)
return null;
else
return annotation.expectedCause();
}
private boolean expectsCauseException( ExtendedTest annotation) {
return getExpectedCauseException(annotation) != null;
}
}
ExtendedTest.java - annotation to mark test methods with:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ExtendedTest
{
/**
* Default empty exception
*/
static class None extends Throwable {
private static final long serialVersionUID= 1L;
private None() {
}
}
Class<? extends Throwable> expectedCause() default None.class;
}
ExpectCauseException.java - new JUnit Statement:
public class ExpectCauseException extends Statement
{
private Statement fNext;
private final Class<? extends Throwable> fExpected;
public ExpectCauseException( Statement next, Class<? extends Throwable> expected )
{
fNext= next;
fExpected= expected;
}
@Override
public void evaluate() throws Exception
{
boolean complete = false;
try {
fNext.evaluate();
complete = true;
} catch (Throwable e) {
if ( e.getCause() == null || !fExpected.isAssignableFrom( e.getCause().getClass() ) )
{
String message = "Unexpected exception cause, expected<"
+ fExpected.getName() + "> but was<"
+ ( e.getCause() == null ? "none" : e.getCause().getClass().getName() ) + ">";
throw new Exception(message, e);
}
}
if (complete)
throw new AssertionError( "Expected exception cause: "
+ fExpected.getName());
}
}
Usage:
@RunWith( ExtendedTestRunner.class )
public class MyTests
{
@ExtendedTest( expectedCause = MyException.class )
public void someMethod()
{
throw new RuntimeException( new MyException() );
}
}
You could always do it manually:
@Test
public void someMethod() {
try{
... all your code
} catch (Exception e){
// check your nested clauses
if(e.getCause() instanceof FooException){
// pass
} else {
Assert.fail("unexpected exception");
}
}
You could create a Matcher for exceptions. This works even when you are using another test runner like Arquillian's @RunWith(Arquillian.class)
so you can't use the @RunWith(ExtendedTestRunner.class)
approach suggested above.
Here's a simple example:
public class ExceptionMatcher extends BaseMatcher<Object> {
private Class<? extends Throwable>[] classes;
// @SafeVarargs // <-- Suppress warning in Java 7. This usage is safe.
public ExceptionMatcher(Class<? extends Throwable>... classes) {
this.classes = classes;
}
@Override
public boolean matches(Object item) {
for (Class<? extends Throwable> klass : classes) {
if (! klass.isInstance(item)) {
return false;
}
item = ((Throwable) item).getCause();
}
return true;
}
@Override
public void describeTo(Description descr) {
descr.appendText("unexpected exception");
}
}
Then use it with @Rule and ExpectedException like this:
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testSomething() {
thrown.expect(new ExceptionMatcher(IllegalArgumentException.class, IllegalStateException.class));
throw new IllegalArgumentException("foo", new IllegalStateException("bar"));
}
Added by Craig Ringer in 2012 edit: An enhanced and more reliable version:
boolean rethrow
to throw unmatched exception. That preserves the stack trace of the nested exceptions for easier debugging.@SaveVarargs
on older versions.Full code:
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
public class ExceptionMatcher extends BaseMatcher<Object> {
private Class<? extends Throwable>[] acceptedClasses;
private Throwable[] nestedExceptions;
private final boolean rethrow;
@SafeVarargs
public ExceptionMatcher(Class<? extends Throwable>... classes) {
this(false, classes);
}
@SafeVarargs
public ExceptionMatcher(boolean rethrow, Class<? extends Throwable>... classes) {
this.rethrow = rethrow;
this.acceptedClasses = classes;
}
@Override
public boolean matches(Object item) {
nestedExceptions = ExceptionUtils.getThrowables((Throwable)item);
for (Class<? extends Throwable> acceptedClass : acceptedClasses) {
for (Throwable nestedException : nestedExceptions) {
if (acceptedClass.isInstance(nestedException)) {
return true;
}
}
}
if (rethrow) {
throw new AssertionError(buildDescription(), (Throwable)item);
}
return false;
}
private String buildDescription() {
StringBuilder sb = new StringBuilder();
sb.append("Unexpected exception. Acceptable (possibly nested) exceptions are:");
for (Class<? extends Throwable> klass : acceptedClasses) {
sb.append("\n ");
sb.append(klass.toString());
}
if (nestedExceptions != null) {
sb.append("\nNested exceptions found were:");
for (Throwable nestedException : nestedExceptions) {
sb.append("\n ");
sb.append(nestedException.getClass().toString());
}
}
return sb.toString();
}
@Override
public void describeTo(Description description) {
description.appendText(buildDescription());
}
}
Typical output:
java.lang.AssertionError: Expected: Unexpected exception. Acceptable (possibly nested) exceptions are:
class some.application.Exception
Nested exceptions found were:
class javax.ejb.EJBTransactionRolledbackException
class javax.persistence.NoResultException
got: <javax.ejb.EJBTransactionRolledbackException: getSingleResult() did not retrieve any entities.>
I wrote a little JUnit extension for that purpose. A static helper function takes a function body and an array of expected exceptions:
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
public class AssertExt {
public static interface Runnable {
void run() throws Exception;
}
public static void assertExpectedExceptionCause( Runnable runnable, @SuppressWarnings("unchecked") Class[] expectedExceptions ) {
boolean thrown = false;
try {
runnable.run();
} catch( Throwable throwable ) {
final Throwable cause = throwable.getCause();
if( null != cause ) {
assertTrue( Arrays.asList( expectedExceptions ).contains( cause.getClass() ) );
thrown = true;
}
}
if( !thrown ) {
fail( "Expected exception not thrown or thrown exception had no cause!" );
}
}
}
You can now check for expected nested exceptions like so:
import static AssertExt.assertExpectedExceptionCause;
import org.junit.Test;
public class TestExample {
@Test
public void testExpectedExceptionCauses() {
assertExpectedExceptionCause( new AssertExt.Runnable(){
public void run() throws Exception {
throw new Exception( new NullPointerException() );
}
}, new Class[]{ NullPointerException.class } );
}
}
This saves you writing the same boiler plate code again and again.
In JUnit5 you can use assertThrows
method which apart of asserting the thrown exception it also returns it so that you can perform additional assertions on it.
@Test
void test() {
// Assert that parent exception is thrown and retrieve it
ParentException parentException = assertThrows(ParentException.class, () -> methodThatThrowsException());
// Perform assertions on the cause
Throwable cause = parentException.getCause();
assertThat(cause, ...);
}
void methodThatThrowsException() {
throw new ParentException("foo", new SomeNestedException("bar"));
}
The most concise syntax is provided by catch-exception:
import static com.googlecode.catchexception.CatchException.*;
catchException(myObj).doSomethingNasty();
assertTrue(caughtException().getCause() instanceof MyException);
We have asserThatThownBy in assertj for all these type of assertions.
assertThatThrownBy(() -> blah()) .isInstanceOf(BlahException.class) .hasCauseExactlyInstanceOf(BlahNestedException.class) .hasRootCauseMessage("some message");
© 2022 - 2024 — McMap. All rights reserved.