2009-05-15 14 views
30

sé que uno puede definir un 'espera' excepción en JUnit, haciendo:JUnit: ¿Es posible 'esperar' una excepción envuelta?

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

Pero ¿y si no es siempre misma excepción lanzada, pero con diferentes 'anidados' causas.

¿Alguna sugerencia?

+1

poco importante nota lateral: se "espera = ...", no "esperar = ..." – hoijui

+2

No puedo creer JUnit 5 al parecer no ha aumentado su sintaxis de anotación para incluir esta. –

Respuesta

21

Puede ajustar el código de prueba en un bloque try/catch, detectar la excepción lanzada, verificar la causa interna, registrar/afirmar/lo que sea, y luego volver a lanzar la excepción (si lo desea).

5

Siempre se puede hacer de forma manual:

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

me escribió una pequeña extensión de JUnit para ese propósito. Una función auxiliar estática tiene un cuerpo de función y una serie de excepciones esperados:

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

Ahora puede comprobar si hay excepciones anidados esperados de este modo:

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

Esto le ahorra escribir el mismo código de placa de la caldera de nuevo y otra vez.

+1

Eso estaría bien, ¡si Java tuviera cierres! Como es, try/catch/getCause() es probablemente menos código de placa de caldera que la creación de clases anónimas! –

7

Si está utilizando la versión más reciente de JUnit se puede ampliar el corredor de prueba por defecto para manejar esto para usted (sin tener que ajustar cada uno de sus métodos en un bloque try/catch)

ExtendedTestRunner.java - nuevo corredor de prueba:

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 - anotación para marcar con los métodos de ensayo:

@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 - nueva Declaración JUnit:

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

Uso:

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

¡Me encanta esta solución! Sin embargo, lamentablemente tengo problemas para compilarlo junto con las pruebas de Groovy JUnit 4. –

+0

Esa es la solución más limpia. Sin embargo, un par de ediciones: ExtendedTestRunner necesita extender SpringJUnit4ClassRunner para admitir adecuadamente el contexto de Spring. También computeTestMethods tiene un tipo de devolución incompatible (debe ser ArrayList). – warden

4

Se puede crear un Matcher excepciones. Esto funciona incluso cuando está utilizando otro corredor de prueba como Arquillian 's @RunWith(Arquillian.class) por lo que no puede utilizar el enfoque @RunWith(ExtendedTestRunner.class) sugerido anteriormente.

Aquí está un ejemplo sencillo:

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

luego usarlo con @Rule y ExpectedException así:

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

Añadido por Craig Ringer en 2012 de edición: Una versión mejorada y más fiable:

  • Basic usa ge sin cambios desde arriba
  • Puede pasar el primer argumento opcional boolean rethrow para lanzar una excepción sin igual. Eso conserva el seguimiento de la pila de las excepciones anidadas para una depuración más fácil.
  • Usos Apache Commons Lang ExcepciónUtiliza para manejar bucles de causa y para manejar el anidamiento de excepción no estándar utilizado por algunas clases de excepciones comunes.
  • auto-describe incluye aceptado excepciones
  • auto-describen en caso de fallo incluye una pila de la causa de la excepción encontró
  • manija Java 7 advertencia. Elimine el @SaveVarargs en versiones anteriores.

código completo:

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

} 

salida 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: <javax.ejb.EJBTransactionRolledbackException: getSingleResult() did not retrieve any entities.> 
+0

Excelente y muy útil respuesta, gracias. Este enfoque es un salvavidas cuando se trabaja con Arquillian para probar los EJB, ya que les gusta ajustar cada excepción no verificada en una EJBException. –

+0

He extendido el ejemplo en la respuesta a algo más completo. –

1

La sintaxis más concisa es proporcionada por catch-exception:

import static com.googlecode.catchexception.CatchException.*; 

catchException(myObj).doSomethingNasty(); 
assertTrue(caughtException().getCause() instanceof MyException); 
66

A partir de JUnit 4.11 se puede utilizar el ExpectedException regla expectCause() Método:

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

debido a los infiernos genéricos de Hamcrest, la línea 6 tiene que verse así: 'expectedException.expectCause (es (IsInstanceOf. instanceOf (SomeNestedException.class)));' pero, aparte de eso, es una solución elegante. – thrau

+1

@ solución de thrau funciona para mí sin el adicional es: 'expectedException.expectCause (IsInstanceOf instanceOf (xception.class SomeNestedE).);' – Jardo

+0

@Jardo Sí, eso es correcto - es() es sólo azúcar sintáctico que pasa a través de el matcher anidado. Vea los documentos aquí: http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/core/Is.html#is(org.hamcrest.Matcher) – Rowan

Cuestiones relacionadas