【问题标题】:JUnit: Possible to 'expect' a wrapped exception?JUnit:可能“期望”一个包装的异常?
【发布时间】:2010-10-26 15:40:09
【问题描述】:

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

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

但是,如果总是抛出相同的异常,但使用不同的“嵌套”呢? 原因

有什么建议吗?

【问题讨论】:

  • 不重要的旁注:它是“expected=...”,而不是“expect=...”
  • 我不敢相信 JUnit 5 显然没有增加它的注释语法来包含这个。

标签: java exception junit


【解决方案1】:

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

【讨论】:

  • 因为 hamcrest 泛型地狱,第 6 行必须看起来像这样:expectedException.expectCause(is(IsInstanceOf.<Throwable>instanceOf(SomeNestedException.class))); 但除此之外,这是一个优雅的解决方案。
  • @thrau 的解决方案对我有用,没有额外的:expectedException.expectCause(IsInstanceOf.<Throwable>instanceOf(SomeNestedE‌​xception.class));
  • @Jardo 是的,没错 - is() 只是传递给嵌套匹配器的语法糖。在此处查看文档:hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/core/…
  • Same docs 告诉有一个匹配器 isA 作为 is(instanceOf(clazz)) 的简写,所以这就足够了:expectedException.expectCause(isA(SomeNestedException.class));
  • 该方法应该被称为org.hamcrest.CoreMatchers.isA。不在Matchers 类中。
【解决方案2】:

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

【讨论】:

  • 这个答案现在已经过时了。使用@Rowan 的回答
【解决方案3】:

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

【讨论】:

  • 我喜欢这个解决方案!但是,遗憾的是,我无法将它与 Groovy JUnit 4 测试结合起来进行编译。
  • 这是最干净的解决方案。几个编辑虽然:ExtendedTestRunner 需要扩展 SpringJUnit4ClassRunner 以正确支持 Spring 上下文。 computeTestMethods 也有不兼容的返回类型(应该是 ArrayList)。
【解决方案4】:

您总是可以手动完成:

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

【讨论】:

    【解决方案5】:

    您可以为异常创建一个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");
        }
    }
    

    然后将它与@RuleExpectedException 一起使用,如下所示:

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

    由 Craig Ringer 于 2012 年编辑添加:增强且更可靠的版本:

    • 上面的基本用法没有改变
    • 可以通过可选的第一个参数boolean rethrow 来抛出不匹配的异常。这样可以保留嵌套异常的堆栈跟踪,以便于调试。
    • 使用Apache Commons LangExceptionUtils 来处理原因循环和一些常见异常类使用的非标准异常嵌套。
    • 自我描述包括接受的例外情况
    • 失败的自我描述包括遇到异常的原因堆栈
    • 处理 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.>
    

    【讨论】:

    • 优秀且非常有帮助的答案 - 谢谢。当使用 Arquillian 测试 EJB 时,这种方法是救命稻草,因为他们喜欢将每个未经检查的异常包装在 EJBException 中。
    • 我已将答案中的示例扩展为更完整的内容。
    【解决方案6】:

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

    这样可以避免您一次又一次地编写相同的样板代码。

    【讨论】:

    • 如果java有闭包那就太好了!与编写匿名类相比,try/catch/getCause() 的样板代码可能更少!
    【解决方案7】:

    最简洁的语法由catch-exception提供:

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

    【讨论】:

      【解决方案8】:

      在 JUnit5 中,您可以使用 assertThrows 方法,除了断言抛出的异常之外,它还会返回它,以便您可以对其执行额外的断言。

      @Test
      void test() {
          // Assert that parent exception is thrown and retrieve it
          ParentException parentException = assertThrows(ParentException.class, () -> methodThatThrowsException());
          // Perform assertions on the cause
          Throwable cause = parentException.getCause();
          assertThat(cause, ...);
      }
      
      void methodThatThrowsException() {
          throw new ParentException("foo", new SomeNestedException("bar"));
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-02-05
        • 2013-12-06
        • 1970-01-01
        相关资源
        最近更新 更多