在Java中如何使用JUnit验证抛出的异常?

In Java how can I validate a thrown exception with JUnit?

在为Java API编写单元测试时,可能会出现需要对异常执行更详细验证的情况。即 比JUnit提供的@test注释提供的更多。

例如,考虑一个应该从其他接口捕获异常的类,包装该异常并抛出包装的异常。 您可能需要验证:

  • 抛出包装异常的确切方法调用。
  • 包装器异常将原始异常作为其原因。
  • 包装器异常的消息。
  • 这里的要点是,您希望在单元测试中对异常进行额外验证(而不是关于是否应该验证异常消息等事项的争论)。

    对此有什么好的方法?


    根据你的答案,这是一个很好的方法。除此之外:

    您可以将函数expectException包装到一个名为ExpectedException的新注释中。
    带注释的方法如下所示:

    1
    2
    3
    4
    5
    @Test
    @ExpectedException(class=WrapperException.class, message="Exception Message", causeException)
    public void testAnExceptionWrappingFunction() {
      //whatever you test
    }

    这种方式更具可读性,但它的方法完全相同。

    另一个原因是:我喜欢Annotations :)


    在JUnit 4中,可以使用ExpectedException规则轻松完成。

    以下是javadocs的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // These tests all pass.
    public static class HasExpectedException {
        @Rule
        public ExpectedException thrown = ExpectedException.none();

        @Test
        public void throwsNothing() {
            // no exception expected, none thrown: passes.
        }

        @Test
        public void throwsNullPointerException() {
            thrown.expect(NullPointerException.class);
            throw new NullPointerException();
        }

        @Test
        public void throwsNullPointerExceptionWithMessage() {
            thrown.expect(NullPointerException.class);
            thrown.expectMessage("happened?");
            thrown.expectMessage(startsWith("What"));
            throw new NullPointerException("What happened?");
        }
    }


    看看提出的答案,你真的可以感受到Java中没有闭包的痛苦。恕我直言,最可读的解决方案是你好老尝试赶上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Test
    public void test() {
        ...
        ...
        try {
            ...
            fail("No exception caught :(");
        }
        catch (RuntimeException ex) {
            assertEquals(Whatever.class, ex.getCause().getClass());
            assertEquals("Message", ex.getMessage());
        }
    }

    对于JUNIT 3.x

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void test(){
       boolean thrown = false;
       try{
          mightThrowEx();
       } catch ( Surprise expected ){
          thrown = true;
          assertEquals("message", expected.getMessage());
       }
      assertTrue(thrown );
    }


    在这篇文章之前,我通过这样做完成了我的异常验证:

    1
    2
    3
    4
    5
    6
    try {
        myObject.doThings();
        fail("Should've thrown SomeException!");
    } catch (SomeException e) {
        assertEquals("something", e.getSomething());
    }

    我花了一些时间考虑这个问题,并想出了以下内容(Java5,JUnit 3.x):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // Functor interface for exception assertion.
    public interface AssertionContainer<T extends Throwable> {
        void invoke() throws T;
        void validate(T throwable);
        Class< T > getType();
    }

    // Actual assertion method.
    public <T extends Throwable> void assertThrowsException(AssertionContainer< T > functor) {
        try {
            functor.invoke();
            fail("Should've thrown"+functor.getType()+"!");
        } catch (Throwable exc) {
            assertSame("Thrown exception was of the wrong type! Expected"+functor.getClass()+", actual"+exc.getType(),
                       exc.getClass(), functor.getType());
            functor.validate((T) exc);
        }
    }

    // Example implementation for servlet I used to actually test this. It was an inner class, actually.
    AssertionContainer<ServletException> functor = new AssertionContainer<ServletException>() {
        public void invoke() throws ServletException {
            servlet.getRequiredParameter(request,"some_param");
        }

        public void validate(ServletException e) {
            assertEquals("Parameter "some_param" wasn't found!", e.getMessage());
        }

        public Class<ServletException> getType() {
            return ServletException.class;
        }
    }

    // And this is how it's used.
    assertThrowsException(functor);

    看着这两个我无法决定哪一个我更喜欢。我想这是其中一个问题,其中实现目标(在我的情况下,使用functor参数的断言方法)从长远来看是不值得的,因为这些6+代码断言尝试要容易得多..catch块。

    再说一次,也许我在星期五晚上解决问题的10分钟结果并不是最聪明的方法。


    @akuhn:

    即使没有闭包,我们也可以获得更易读的解决方案(使用catch-exception):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import static com.googlecode.catchexception.CatchException.*;

    public void test() {
        ...
        ...
        catchException(nastyBoy).doNastyStuff();
        assertTrue(caughtException() instanceof WhateverException);
        assertEquals("Message", caughtException().getMessage());
    }


    我做了一件很简单的事

    1
    2
    3
    4
    5
    6
    7
    8
    testBla(){
        try {
            someFailingMethod()
            fail(); //method provided by junit
        } catch(Exception e) {
              //do nothing
        }
    }


    以下帮助方法(改编自此博客文章)可以解决问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /**
     * Run a test body expecting an exception of the
     * given class and with the given message.
     *
     * @param test              To be executed and is expected to throw the exception.
     * @param expectedException The type of the expected exception.
     * @param expectedMessage   If not null, should be the message of the expected exception.
     * @param expectedCause     If not null, should be the same as the cause of the received exception.
     */

    public static void expectException(
            Runnable test,
            Class<? extends Throwable> expectedException,
            String expectedMessage,
            Throwable expectedCause) {
        try {
            test.run();
        }
        catch (Exception ex) {
            assertSame(expectedException, ex.getClass());
            if (expectedMessage != null) {
                assertEquals(expectedMessage, ex.getMessage());
            }

            if (expectedCause != null) {
                assertSame(expectedCause, ex.getCause());
            }

            return;
        }

        fail("Didn't find expected exception of type" + expectedException.getName());
    }

    然后,测试代码可以如下调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    TestHelper.expectException(
            new Runnable() {
                public void run() {
                    classInstanceBeingTested.methodThatThrows();
                }
            },
            WrapperException.class,
           "Exception Message",
            causeException
    );


    对于JUnit 5,它更容易:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        @Test
        void testAppleIsSweetAndRed() throws Exception {

            IllegalArgumentException ex = assertThrows(
                    IllegalArgumentException.class,
                    () -> testClass.appleIsSweetAndRed("orange","red","sweet"));

            assertEquals("this is the exception message", ex.getMessage());
            assertEquals(NullPointerException.class, ex.getCause().getClass());
        }

    通过返回异常对象本身,assertThrows()允许您测试有关抛出异常的每个方面。


    我做了一个类似于其他帖子的帮手:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class ExpectExceptionsExecutor {

        private ExpectExceptionsExecutor() {
        }

        public static  void execute(ExpectExceptionsTemplate e) {
            Class<? extends Throwable> aClass = e.getExpectedException();

            try {
                Method method = ExpectExceptionsTemplate.class.getMethod("doInttemplate");
                method.invoke(e);
            } catch (NoSuchMethodException e1) {


                throw new RuntimeException();
            } catch (InvocationTargetException e1) {


                Throwable throwable = e1.getTargetException();
                if (!aClass.isAssignableFrom(throwable.getClass())) {
                    //  assert false
                    fail("Exception isn't the one expected");
                } else {
                    assertTrue("Exception captured", true);
                    return;
                }
                ;


            } catch (IllegalAccessException e1) {
                throw new RuntimeException();
            }

            fail("No exception has been thrown");
        }


    }

    以及客户端应该实现的模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface ExpectExceptionsTemplate<T extends Throwable> {


        /**
         * Specify the type of exception that doInttemplate is expected to throw
         * @return
         */

        Class< T > getExpectedException();


        /**
         * Execute risky code inside this method
         * TODO specify expected exception using an annotation
         */

        public void doInttemplate();

    }

    客户端代码将是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
        public void myTest() throws Exception {
            ExpectExceptionsExecutor.execute(new ExpectExceptionsTemplate() {
                @Override
                public Class getExpectedException() {
                    return IllegalArgumentException.class;
                }

                @Override
                public void doInttemplate() {
                    riskyMethod.doSomething(null);
                }
            });
         }

    它看起来真的很冗长但如果你使用具有良好自动完成功能的IDE,你只需要编写异常类型和正在测试的实际代码。 (其余的将由IDE完成:D)