web-dev-qa-db-ja.com

JUnit:ラップされた例外を「期待する」ことは可能ですか?

JUnitで'expected'例外を定義できることを知っています。

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

しかし、常に同じ例外がスローされるが、「ネストされた」異なる原因の場合はどうなるでしょうか。

助言がありますか?

テストコードをtry/catchブロックでラップし、スローされた例外をキャッチし、内部原因を確認し、ログ/アサート/何でもして、(必要に応じて)例外を再スローできます。

24
Paul Sonier

JUnit 4.11以降では、ExpectedExceptionルールの expectCause() メソッドを使用できます。

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

JUnitの最新バージョンを使用している場合は、デフォルトのテストランナーを拡張してこれを処理できます(try/catchブロックで各メソッドをラップする必要はありません)。

ExtendedTestRunner.Java-新しいテストランナー:

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-テストメソッドをマークする注釈:

@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-新しい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());
    }
}

使用法:

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

あなたはいつでもそれを手動で行うことができます:

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

例外のために Matcher を作成できます。これは Arquillian@RunWith(Arquillian.class)などの別のテストランナーを使用している場合でも機能するため、上記の@RunWith(ExtendedTestRunner.class)アプローチを使用できません。

以下に簡単な例を示します。

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

次に、これを @ Rule および ExpectedException と次のように使用します。

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

2012年にCraig Ringerによって追加されましたedit:拡張されたより信頼性の高いバージョン:

  • 上と同じ基本的な使用法
  • オプションの第1引数boolean rethrowを渡して、一致しない例外をスローできます。これにより、ネストされた例外のスタックトレースが保持され、デバッグが容易になります。
  • Apache Commons Lang ExceptionUtilsを使用して原因ループを処理し、一部の一般的な例外クラスで使用される非標準の例外のネストを処理します。
  • 自己記述には認められた例外が含まれます
  • 失敗時の自己記述には、発生した例外の原因スタックが含まれます
  • ハンドルJava 7警告。古いバージョンでは@SaveVarargsを削除してください。

完全なコード:

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

}

典型的な出力:

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.>
5
Jesse Merriman

そのために、JUnit拡張機能を少し書きました。静的ヘルパー関数は、関数本体と予期される例外の配列を取ります。

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

次のように、予想されるネストされた例外を確認できます。

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

これにより、同じボイラープレートコードを何度も書く手間が省けます。

4
digitalbreed

最も簡潔な構文は catch-exception によって提供されます:

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

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