2009-05-15 7 views
30

So che si può definire un un'eccezione 'previsto' in JUnit, facendo:JUnit: è possibile "aspettarsi" un'eccezione avvolta?

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

Ma cosa succede se c'è sempre la stessa eccezione generata, ma con diverse 'annidate' cause.

Qualche suggerimento?

+1

poco importante side-nota: si "aspetta = ...", non "aspettarsi = ..." – hoijui

+2

Non posso credere che JUnit 5 a quanto pare non ha aumentato la sua sintassi di annotazione di includere Questo. –

risposta

21

È possibile avvolgere il codice di prova in un blocco try/catch, rilevare l'eccezione generata, controllare la causa interna, registrare/asserire/qualsiasi altra cosa e quindi rilanciare l'eccezione (se lo si desidera).

5

Si può sempre farlo 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"); 
     } 
    } 
4

ho scritto un po 'di estensione JUnit a tale scopo. Una funzione di supporto statica prende corpo di una funzione e una serie di eccezioni attesi:

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

È ora possibile verificare la presenza di eccezioni nidificate che ci si attende in questo 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 }); 
    } 
} 

Ciò consente di risparmiare a scrivere di nuovo lo stesso codice piastra caldaia e di nuovo.

+1

Sarebbe bello, se java avesse delle chiusure! Come è, try/catch/getCause() è probabilmente un codice di piastra di caldaia inferiore rispetto alla creazione di classi anonime! –

7

Se si sta utilizzando l'ultima versione di JUnit è possibile estendere il test runner di default per gestire questo per voi (senza dover avvolgere ciascuno dei vostri metodi in un blocco try/catch)

ExtendedTestRunner.java - nuovo 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 - annotazione per contrassegnare i metodi di prova con:

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

Usage:

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

Adoro questa soluzione! Tuttavia, purtroppo ho problemi a compilarlo in combinazione con Groovy JUnit 4 testing. –

+0

Questa è la soluzione più pulita. Un paio di modifiche però: ExtendedTestRunner deve estendere SpringJUnit4ClassRunner per supportare correttamente il contesto Spring. Anche computeTestMethods ha un tipo di ritorno incompatibile (dovrebbe essere ArrayList). – warden

4

si potrebbe creare un Matcher per le eccezioni. Funziona anche quando stai utilizzando un altro test runner come Arquillian@RunWith(Arquillian.class), quindi non puoi usare l'approccio @RunWith(ExtendedTestRunner.class) suggerito sopra.

Ecco un semplice esempio:

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

quindi utilizzarlo con @Rule e ExpectedException come questo:

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

Aggiunto da Craig Ringer nel 2012 edit: una versione migliorata e più affidabile:

  • Stati Uniti di base ge invariato dall'alto
  • Può passare opzionale 1o argomento boolean rethrow per generare un'eccezione senza eguali. Conserva la traccia dello stack delle eccezioni nidificate per semplificare il debug.
  • Utilizza Apache Commons Lang ExceptionUtils per gestire i loop di causa e per gestire l'annidamento di eccezioni non standard utilizzato da alcune classi di eccezioni comuni.
  • Self-descrivono include accettato eccezioni
  • Self-descrivere in caso di fallimento include una la causa pila di eccezione incontrato
  • maniglia Java 7 avvertimento. Rimuovere lo @SaveVarargs nelle versioni precedenti.

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

} 

uscita tipico:

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

Risposta eccellente e molto utile - grazie. Questo approccio è un vero toccasana quando si lavora con Arquillian per testare gli EJB, dal momento che a loro piace avvolgere ogni eccezione non controllata in un EJBException. –

+0

Ho esteso l'esempio nella risposta a qualcosa di più completo. –

1

La sintassi più concisa è fornita da catch-exception:

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

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

Come di JUnit 4.11 è possibile utilizzare il ExpectedException regole expectCause() Metodo:

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

a causa dell'inferno generico di hamcrest, la riga 6 deve apparire così: 'expectedException.expectCause (is (IsInstanceOf. instanceOf (SomeNestedException.class)))' ma, a parte questo, è una soluzione elegante. – thrau

+1

La soluzione di @ thrau funziona per me senza l'ulteriore è: 'expectedException.expectCause (IsInstanceOf. instanceOf (SomeNestedE xception.class));' – Jardo

+0

@Jardo Sì, è vero - is() è solo lo zucchero sintattico che passa a l'abbinatore annidato. Vedi i documenti qui: http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/core/Is.html#is(org.hamcrest.Corrispondente) – Rowan