Java JUnit:可能是';期望';包装异常?

Java JUnit:可能是';期望';包装异常?,java,exception,junit,Java,Exception,Junit,我知道可以在JUnit中定义一个'expected'异常,这样做: @Test(expect=MyException.class) public void someMethod() { ... } 但如果总是抛出相同的异常,但使用不同的“嵌套”呢 原因 有什么建议吗?您可以将测试代码包装在try/catch块中,捕获抛出的异常,检查内部原因,记录/断言/任何内容,然后重新抛出异常(如果需要)。您可以随时手动执行: @Test public void someMethod() { try

我知道可以在JUnit中定义一个'expected'异常,这样做:

@Test(expect=MyException.class)
public void someMethod() { ... }
但如果总是抛出相同的异常,但使用不同的“嵌套”呢 原因


有什么建议吗?

您可以将测试代码包装在try/catch块中,捕获抛出的异常,检查内部原因,记录/断言/任何内容,然后重新抛出异常(如果需要)。

您可以随时手动执行:

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

为此,我编写了一个小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 } );
    }
}

这样可以节省您一次又一次地编写相同的锅炉板代码。

如果您使用的是最新版本的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;
    }

}
您可以为异常创建一个。即使您正在使用另一个测试运行程序,例如的
@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");
    }
}
由Craig Ringer于2012年添加编辑:增强版和更可靠版:

  • 基本用法同上
  • 可以传递可选的第一个参数
    布尔重试
    以引发不匹配的异常。这将保留嵌套异常的堆栈跟踪,以便于调试
  • 使用ExceptionUtils来处理原因循环和处理某些常见异常类使用的非标准异常嵌套
  • 自我描述包括可接受的例外情况
  • 故障时自我描述包括遇到异常的原因堆栈
  • 处理Java7警告。在旧版本上删除
    @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());
    }

}
import org.apache.commons.lang3.exception.ExceptionUtils;
导入org.hamcrest.BaseMatcher;
导入org.hamcrest.Description;
公共类ExceptionMatcher扩展了BaseMatcher{

private Class提供了最简洁的语法:


从JUnit 4.11开始,您可以使用
ExpectedException
规则的方法:


如果java有闭包,那就太好了!按原样,试试/catch/getCause()与制作匿名类相比,可能更少的是锅炉板代码!我喜欢这个解决方案!但是,遗憾的是,我很难让它与Groovy JUnit 4测试一起编译。这是最干净的解决方案。尽管有两个编辑:ExtendedTestRunner需要扩展SpringJUnit4ClassRunner以正确支持Spring上下文。另外,计算teTestMethods具有不兼容的返回类型(应为ArrayList)。非常好而且非常有用的答案-谢谢。当使用Arquillian测试EJB时,这种方法是一种救命稻草,因为他们喜欢将每个未检查的异常都包装在一个EJBException中。我已经将答案中的示例扩展为更完整的内容。由于hamcrest泛型地狱,第6行必须如下所示:
expectedException.eexpectCause(is(IsInstanceOf.instanceOf(SomeNestedException.class));
除此之外,它是一个优雅的解决方案。@thrau的解决方案对我来说很有效,没有额外的is:
expectedException.expectCause(IsInstanceOf.instanceOf(SomeNestedE))‌​是的,没错-is()只是传递给嵌套匹配器的语法糖。请参阅此处的文档:告诉我们有一个匹配器
isA
作为
is(instanceOf(clazz))
,这样就足够了:
expectedException.expectCause(isA(SomeNestedException.class));
应该调用该方法。不在
Matchers
类中。不重要的旁注:它是“expected=…”,而不是“expected=…”。我不敢相信JUnit 5显然没有扩展其注释语法以包含此内容。此答案现在已过时。请使用@Rowan的答案
@RunWith( ExtendedTestRunner.class )
public class MyTests
{
    @ExtendedTest( expectedCause = MyException.class )
    public void someMethod()
    {
        throw new RuntimeException( new MyException() );
    }
}
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
public ExpectedException thrown = ExpectedException.none();

@Test
public void testSomething() {
    thrown.expect(new ExceptionMatcher(IllegalArgumentException.class, IllegalStateException.class));

    throw new IllegalArgumentException("foo", new IllegalStateException("bar"));
}
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.>
import static com.googlecode.catchexception.CatchException.*;

catchException(myObj).doSomethingNasty();
assertTrue(caughtException().getCause() instanceof MyException);
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"));
}