【问题标题】:Mark unit test as an expected failure in JUnit将单元测试标记为 JUnit 中的预期失败
【发布时间】:2011-05-02 14:03:40
【问题描述】:

如何在 JUnit 4 中将测试标记为预期失败?

在这种情况下,我想继续运行这个测试,直到上游修补了一些东西。忽略测试有点过分,因为那时我可能会忘记它。我也许可以添加一个@expected 注释并捕获assertThat 抛出的异常,但这似乎也与预期的行为有关。

这是我当前的测试结果:

@Test
public void unmarshalledDocumentHasExpectedValue() 
{
    doc = unmarshaller.unmarshal(getResourceAsStream("mydoc.xml"));
    final ST title = doc.getTitle();
    assertThat(doc.getTitle().toStringContent(), equalTo("Expected"));
}

该断言应该成功,但由于上游错误,它没有成功。然而,该测试是正确的;它应该成功。我发现的几乎所有替代方案都具有误导性。现在我认为@Ignore("This test should pass once fixed upstream") 是我最好的选择,但我仍然必须记得回到它。我更喜欢测试运行。

在 Python 中,我可以使用 expectedFailure 装饰器:

class ExpectedFailureTestCase(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "broken")

在C++中使用Qt的QTestLib,你可以使用QEXPECT_FAIL

QEXPECT_FAIL("", "Will be fixed next version", Continue);
QCOMPARE(i, 42);

在上述两种情况下,单元测试都会运行,这是我希望发生的。我在 JUnit 中遗漏了什么吗?

【问题讨论】:

    标签: java unit-testing junit


    【解决方案1】:

    我不太了解您的场景的具体情况,但这是我通常测试预期失败的方式:

    巧妙的新方法:

    @Test(expected=NullPointerException.class)
    public void expectedFailure() {
        Object o = null;
        o.toString();
    }
    

    对于旧版本的 JUnit:

    public void testExpectedFailure() {
        try {
            Object o = null;
            o.toString();
            fail("shouldn't get here");
        }
        catch (NullPointerException e) {
            // expected
        }
    }
    

    如果您有很多事情要确保引发异常,您可能还想在循环中使用第二种技术,而不是为每种情况创建单独的测试方法。如果您只是使用expected 在一个方法中循环遍历一堆案例,那么第一个抛出异常的案例将结束测试,并且不会检查后续案例。

    【讨论】:

    • 最好由@Test(expected=NullPointerException.class) 处理。然后你可以删除 try/catch 块和失败语句,因为 JUnit 会告诉你它预期一个异常并且如果没有抛出一个异常就不会收到。
    • 确实如此。我忘了他们已经添加了。
    【解决方案2】:

    明确地期待 AssertionError 怎么样?

    @Test(expected = AssertionError.class)
    public void unmarshalledDocumentHasExpectedValue() {
        // ...
    }
    

    如果您有理由相信只有测试中的 JUnit 机制会引发 AssertionError,那么这似乎是自记录的。

    您仍然会冒忘记这样的测试的风险。如果有的话,我不会让这样的测试长期进入版本控制。

    【讨论】:

    • 好主意!除非有更好的答案出现,否则我会在几天内接受。 (如果我还没有接受答案,因为我忘记了,然后 ping 我)。
    • 这可能是最好的选择......但是当断言成功时测试将“失败”感觉很脏。
    • @SamuelEdwinWard 同意这感觉很脏......但是,我只会将它用于短暂的故障。如果它们持续时间超过几个 SCM 签入周期,则测试可能已过时,因此我将其删除。
    • 我发现这个问题是因为一个不同的要求:我有一个正常的测试用例(其中有一些断言),如果在一个特定的方法之后调用一个方法,它就会成功。我想要另一个几乎相同的测试,但没有臭名昭著的方法,那肯定会失败。我当然可以否定每一个陈述,但是“不那么脏”恕我直言,展示两个不同的测试一个成功,另一个失败
    【解决方案3】:

    我在这里假设如果您的断言失败,您希望测试通过,但如果断言成功,那么测试也应该通过。

    最简单的方法是使用TestRule。 TestRule 提供了在运行测试方法之前和之后执行代码的机会。这是一个例子:

    public class ExpectedFailureTest {
        public class ExpectedFailure implements TestRule {
            public Statement apply(Statement base, Description description) {
                return statement(base, description);
            }
    
            private Statement statement(final Statement base, final Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        try {
                            base.evaluate();
                        } catch (Throwable e) {
                            if (description.getAnnotation(Deprecated.class) != null) {
                                // you can do whatever you like here.
                                System.err.println("test failed, but that's ok:");
                            } else {
                                throw e;
                            }
                        }
                    }
                };
            }
        }
    
        @Rule public ExpectedFailure expectedFailure = new ExpectedFailure();
    
        // actually fails, but we catch the exception and make the test pass.
        @Deprecated
        @Test public void testExpectedFailure() {
            Object o = null;
            o.equals("foo");
        }
    
        // fails
        @Test public void testExpectedFailure2() {
            Object o = null;
            o.equals("foo");
        }
    }
    

    首先,注意第一个方法被标记为@Deprecated。我使用它作为我想忽略任何断言失败的方法的标记。你可以做任何你喜欢的方法来识别方法,这只是一个例子。

    接下来,在ExpectedFailure#apply() 中,当我执行 base.evaluate() 时,我正在捕获任何 Throwable(包括 AssertionError),并且如果该方法标有注释 @Deprecated,我将忽略该错误。您可以根据版本号、某些文本等执行您喜欢的任何逻辑来决定是否应该忽略错误。您还可以将动态确定的标志传递给 ExpectedFailure 以允许它在某些版本号下失败:

    public void unmarshalledDocumentHasExpectedValue() {
        doc = unmarshaller.unmarshal(getResourceAsStream("mydoc.xml"));
    
        expectedFailure.setExpectedFailure(doc.getVersionNumber() < 3000);
    
        final ST title = doc.getTitle();
        assertThat(doc.getTitle().toStringContent(), equalTo("Expected"));
    }
    

    更多示例,请参阅ExternalResourceExpectedException

    忽略预期的失败测试而不是通过它

    如果你想将你的测试标记为 Ignored 而不是 Success,它会变得有点复杂,因为测试在执行之前会被忽略,所以你必须回顾性地将测试标记为被忽略,这将涉及构建你自己的 Runner .首先,请参阅我对How to define JUnit method rule in a suite? 的回答。或者问另一个问题。

    【讨论】:

    • 太棒了!只需创建一个注释,例如 ExpectedFailure(behavior=SucceedWhenTestFails) 并使用它而不是 Deprecated,测试将完全可读且有据可查。
    • 别忘了在你的注解中加上@Retention(RetentionPolicy.RUNTIME),否则运行时是看不到的!
    【解决方案4】:

    一个选项是将测试标记为@Ignore 并将可能存在错误并等待修复的文本放入其中。这样就不会跑了。然后它将被跳过。您还可以使用extensions 以可能不同的方式满足您的需求。

    【讨论】:

      【解决方案5】:

      我已经将 Matthew 的回答更进一步,实际上实现了一个 @Optional 注释,您可以使用它来代替他在回答中提到的 @Deprecated 标记注释。虽然很简单,但还是把代码分享给大家,或许对大家有帮助:

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface Optional {
      
        /**
         * Specify a Throwable, to cause a test method to succeed even if an exception
         * of the specified class is thrown by the method.
         */
        Class<? extends Throwable>[] exception();
      }
      

      对 Matt 的 ExpectedFailure 类进行简单的修改:

      public class ExpectedFailure implements TestRule {
      
        @Override
        public Statement apply(final Statement base, final Description description) {
          return statement(base, description);
        }
      
        private Statement statement(final Statement base, final Description description) {
          return new Statement() {
      
            @Override
            public void evaluate() throws Throwable {
              try {
                base.evaluate();
              } catch (Throwable e) {
                // check for certain exception types
                Optional annon = description.getAnnotation(Optional.class);
                if (annon != null && ArrayUtils.contains(annon.exception(), e.getClass())) {
                  // ok
                } else {
                  throw e;
                }
              }
            }
          };
        }
      }
      

      您现在可以使用 @Optional 注释您的测试方法,即使引发给定类型的异常(提供您希望测试方法通过的一种或多种类型),它也不会失败:

      public class ExpectedFailureTest {
      
        @Rule public ExpectedFailure expectedFailure = new ExpectedFailure();
      
        // actually fails, but we catch the exception and make the test pass.
        @Optional(exception = NullPointerException.class)
        @Test public void testExpectedFailure() {
            Object o = null;
            o.equals("foo");
        }
      
      }
      

      [更新]

      如果您希望测试通过,即使假设不成立,您也可以使用 JUnit 的 org.junit.Assume 而不是传统的 org.junit.Assert 重写您的测试。

      来自Assume的JavaDoc:

      一组方法可用于陈述关于测试有意义的条件的假设。失败的假设并不意味着代码被破坏,而是测试没有提供有用的信息。默认的 JUnit 运行器将假设失败的测试视为忽略。

      Assume 自 JUnit 4.4 起可用

      【讨论】:

        【解决方案6】:

        尽可能使用模拟的上游类。以正确的结果存根。可选地,在 bug 修复后将 mock 替换为真实对象。

        【讨论】:

        • 在你这样做之后,你可能不会再回到真实的对象了:-)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多