JUnit: Possível ‘esperar’ uma exceção empacotada?

Eu sei que se pode definir uma exceção ‘esperada’ no JUnit, fazendo:

@Test(expect=MyException.class) public void someMethod() { ... } 

Mas e se houver sempre a mesma exceção lançada, mas com causas ‘aninhadas’ diferentes?

Alguma sugestão?

Você pode envolver o código de teste em um bloco try / catch, capturar a exceção gerada, verificar a causa interna, log / assert / whatever e, em seguida, retroceder a exceção (se desejado).

A partir do JUnit 4.11 você pode usar o método expectCause() da regra ExpectedException :

 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")); } 

Se você estiver usando a versão mais recente do JUnit, poderá estender o executor de teste padrão para lidar com isso para você (sem precisar envolver cada um dos seus methods em um bloco try / catch)

ExtendedTestRunner.java – Novo corredor de teste:

 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 computeTestMethods() { Set testMethods = new HashSet( super.computeTestMethods() ); testMethods.addAll( getTestClass().getAnnotatedMethods( ExtendedTest.class ) ); return testMethods; } @Override protected void validateTestMethods( List errors ) { super.validateTestMethods( errors ); validatePublicVoidNoArgMethods( ExtendedTest.class, false, errors ); } private Class 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 – anotação para marcar methods de teste com:

 @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 expectedCause() default None.class; } 

ExpectCauseException.java – nova declaração JUnit:

 public class ExpectCauseException extends Statement { private Statement fNext; private final Class fExpected; public ExpectCauseException( Statement next, Class 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()); } } 

Uso:

 @RunWith( ExtendedTestRunner.class ) public class MyTests { @ExtendedTest( expectedCause = MyException.class ) public void someMethod() { throw new RuntimeException( new MyException() ); } } 

Você sempre pode fazer isso manualmente:

 @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"); } } 

Você poderia criar um Matcher para exceções. Isso funciona mesmo quando você está usando outro @RunWith(Arquillian.class) testes, como o @RunWith(Arquillian.class) então você não pode usar a @RunWith(ExtendedTestRunner.class) sugerida acima.

Aqui está um exemplo simples:

 public class ExceptionMatcher extends BaseMatcher { private Class[] classs; // @SafeVarargs // <-- Suppress warning in Java 7. This usage is safe. public ExceptionMatcher(Class... classs) { this.classs = classs; } @Override public boolean matches(Object item) { for (Class klass : classs) { if (! klass.isInstance(item)) { return false; } item = ((Throwable) item).getCause(); } return true; } @Override public void describeTo(Description descr) { descr.appendText("unexpected exception"); } } 

Em seguida, use-o com @Rule e ExpectedException assim:

 @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")); } 

Adicionado por Craig Ringer em 2012 edit: Uma versão melhorada e mais confiável:

  • Uso básico inalterado de cima
  • Pode passar o boolean rethrow 1º argumento opcional para lançar uma exceção incomparável. Isso preserva o rastreamento de pilha das exceções aninhadas para facilitar a debugging.
  • Usa o ExceptionUtils do Apache Commons Lang para manipular loops de causa e manipular o aninhamento de exceção não padrão usado por algumas classs de exceção comuns.
  • Autodescrito inclui exceções aceitas
  • Autodescrito na falha inclui uma pilha de causas da exceção encontrada
  • Lidar com o aviso do Java 7. Remova o @SaveVarargs em versões mais antigas.

Código completo:

 import org.apache.commons.lang3.exception.ExceptionUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; public class ExceptionMatcher extends BaseMatcher { private Class[] acceptedClasses; private Throwable[] nestedExceptions; private final boolean rethrow; @SafeVarargs public ExceptionMatcher(Class... classs) { this(false, classs); } @SafeVarargs public ExceptionMatcher(boolean rethrow, Class... classs) { this.rethrow = rethrow; this.acceptedClasses = classs; } @Override public boolean matches(Object item) { nestedExceptions = ExceptionUtils.getThrowables((Throwable)item); for (Class 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 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()); } } 

Saída típica:

 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:  

Eu escrevi uma pequena extensão JUnit para esse propósito. Uma function auxiliar estática usa um corpo de function e uma matriz de exceções esperadas:

 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!" ); } } } 

Agora você pode verificar exceções aninhadas esperadas como:

 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 } ); } } 

Isso evita que você escreva o mesmo código de placa de caldeira repetidas vezes.

A syntax mais concisa é fornecida pela exceção de captura :

 import static com.googlecode.catchexception.CatchException.*; catchException(myObj).doSomethingNasty(); assertTrue(caughtException().getCause() instanceof MyException);