JUnit:可以“预期” 一个包装的异常?

发布于 2024-07-20 15:57:19 字数 231 浏览 8 评论 0原文

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

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

但是如果总是抛出相同的异常,但具有不同的“嵌套”怎么办? 原因

有什么建议么?

I know that one can define an 'expected' exception in JUnit, doing:

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

But what if there is always same exception thrown, but with different 'nested'
causes.

Any suggestions?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

删除→记忆 2024-07-27 15:57:19

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

As of JUnit 4.11 you can use the ExpectedException rule's expectCause() method:

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"));
}
雪花飘飘的天空 2024-07-27 15:57:19

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

You could wrap the testing code in a try / catch block, catch the thrown exception, check the internal cause, log / assert / whatever, and then rethrow the exception (if desired).

懵少女 2024-07-27 15:57:19

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

If you're using the latest version of JUnit you can extend the default test runner to handle this for you (without having to wrap each of your methods in a try/catch block)

ExtendedTestRunner.java - New 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 - annotation to mark test methods with:

@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 - new JUnit Statement:

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() );
    }
}
不忘初心 2024-07-27 15:57:19

您始终可以手动执行此操作:

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

You could always do it manually:

@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");
        }
    }
醉城メ夜风 2024-07-27 15:57:19

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

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

  • 基本用法与上面相同
  • 可以传递可选的第一个参数布尔重新抛出来抛出不匹配的异常。 这会保留嵌套异常的堆栈跟踪,以便于调试。
  • 使用 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.>

You could create a Matcher for exceptions. This works even when you are using another test runner like Arquillian's @RunWith(Arquillian.class) so you can't use the @RunWith(ExtendedTestRunner.class) approach suggested above.

Here's a simple example:

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

Then use it with @Rule and ExpectedException like this:

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

Added by Craig Ringer in 2012 edit: An enhanced and more reliable version:

  • Basic usage unchanged from above
  • Can pass optional 1st argument boolean rethrow to throw unmatched exception. That preserves the stack trace of the nested exceptions for easier debugging.
  • Uses Apache Commons Lang ExceptionUtils to handle cause loops and to handle non-standard exception nesting used by some common exception classes.
  • Self-describe includes accepted exceptions
  • Self-describe on failure includes a the cause stack of the exception encountered
  • Handle Java 7 warning. Remove the @SaveVarargs on older versions.

Full code:

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

}

Typical output:

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.>
望她远 2024-07-27 15:57:19

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

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

I wrote a little JUnit extension for that purpose. A static helper function takes a function body and an array of expected exceptions:

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

You can now check for expected nested exceptions like so:

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

This saves you writing the same boiler plate code again and again.

说好的呢 2024-07-27 15:57:19

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

In JUnit5 you can use assertThrows method which apart of asserting the thrown exception it also returns it so that you can perform additional assertions on it.

@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"));
}
森罗 2024-07-27 15:57:19

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

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

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

The most concise syntax is provided by catch-exception:

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

catchException(myObj).doSomethingNasty();
assertTrue(caughtException().getCause() instanceof MyException);
凝望流年 2024-07-27 15:57:19

对于所有这些类型的断言,我们在assertj 中有asserThatTownBy。

assertThatThrownBy(() -> blah())
.isInstanceOf(BlahException.class)
.hasCauseExactlyInstanceOf(BlahNestedException.class)
.hasRootCauseMessage("一些消息");

We have asserThatThownBy in assertj for all these type of assertions.

assertThatThrownBy(() -> blah())
.isInstanceOf(BlahException.class)
.hasCauseExactlyInstanceOf(BlahNestedException.class)
.hasRootCauseMessage("some message");

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文