【问题标题】:Semantic for broken unit test破坏单元测试的语义
【发布时间】:2012-07-20 19:53:26
【问题描述】:

我正在为我们的主要产品编写单元测试,并且有一个问题:如何区分

  • 由于测试内容出错而失败的测试(例如,发现了一个错误,并且对其进行了非回归测试)
  • 测试失败,因为测试的另一个意外部分失败(因为测试错误或出现未知错误)

对于第一个,我们肯定有 jUnit Assert 框架,但是对于第二个,我们有什么?

示例:我的单元测试正在测试 c() 不会抛出 MyException,但要执行 c(),我需要先执行 a(),然后执行 b(),两者都可以抛出 MyException(),所以我会写:

@Test
public void testC() {
  a();
  Object forC = b();
  try {
    c(forC);
  } catch (MyException e) {
    Assert.fail("....");
  }
}

但是我需要处理可以被 a 或 b 抛出的 MyException,并且还要处理 forC 不应该为 null 的事实。最好的方法是什么?

  • Catch MyException 由 a 或 b 和 Assert.fail 引发,但 a 和 b 未通过此测试进行测试,因此对我而言,当它们失败时不应将它们标记为测试失败。也许他们稍后会失败,因为此时我们应该做 b();a() 而不是 a();b();。
  • 让 testC 抛出 MyException,这样测试将失败并显示“MyException”,但这是一种误导,因为 MyException 不会告诉您测试是否写错了。然后,所有测试都会因各自的异常而失败。在这种情况下,如果 forC 为 null,我还需要抛出 NullPointerException 之类的东西,这也没有语义。
  • 捕获 a 和 b 抛出的 MyException 并将其包装到一个异常中,该异常表明测试可能是错误的,例如 TestCorruptedException。但是我在 jUnit 中找不到这样的异常,所以 jUnit 不会识别它们(这对我来说没问题)。此外,我需要从我的所有单元测试中知道这个异常,这些单元测试当然被分成多个模块、项目等......所以这是可行的,但会增加依赖项。

您对这个问题的解决方案是什么?我可能会选择第二个,但如上所述,我对它不满意。

【问题讨论】:

    标签: java unit-testing junit


    【解决方案1】:

    单元测试的基石是测试所需的最少代码量,否则它可能会落入您正在寻找端到端功能的集成测试空间。

    如果你能证明a()可以在它自己的测试类中创建和测试,并且b()也可以在它自己的测试类中创建和测试,那么你上面的测试可以忽略测试a()b(),而是使用不会失败的已知值。这通常可以通过使用mock objects 来满足。

    将 a() 和 b() 创建为模拟对象,c() 可以单独测试。如果您发现无法单独测试 c() 和 a() 和 b(),那么这表明您的代码需要更改以便分离关注点。这通常由 a() 和 b() 转换为 c() 的 injecting the dependany 来满足。

    when to use mock objects in unit testing 上的这篇文章可能有助于进一步了解这个主题。

    【讨论】:

    • 对,我的问题确实是 c 与 a() 紧密相关,因为它们的真实名称是 startTransaction() 和 commit(),因此它们具有共同的结构和锁。你的回答告诉我,我正在做一个集成测试,所以我注定要失败,只能祈祷 a 和 b 是正确的。让他们独立需要付出太多努力,但这对我来说是解决问题的方法。
    • 这就是像Spring's annotation driven Transaction Management 这样的声明式事务管理真正大放异彩的地方。您的方法 c() 可以丢弃对 startTransaction() 和 commit() 的调用,让您可以自行测试它。 c() 不在乎它是否在事务中,您将在调用方法中捕获任何异常。这是一个bite sized tutorial 感兴趣
    【解决方案2】:

    聪明人的一句话——在单元测试中永远不要这样做

     try {
        c(forC);
      } catch (MyException e) {
        Assert.fail("....");
      }
    

    这对于任何必须处理您的代码的人来说都有一个非常烦人的副作用:异常的原始原因丢失了。这意味着修复开发人员将进入并调试测试或删除 try/catch 块以查看异常的根本原因。

    如果测试抛出异常并且不应该,让它发生。测试将失败,测试运行的开发人员可以很容易地在控制台/日志中首先看到导致异常的原因。采纳@piotrek 的建议,让您的测试尽可能简单。我已经数不清有多少次我不得不从格式错误的测试中剔除这个构造。

    【讨论】:

      【解决方案3】:

      著名问题:谁来测试我们的测试?简短的回答:你无法区分。您始终可以定义测试之间的依赖关系(在某些测试框架中)。当一项测试失败时,所有依赖于它的测试都不会执行。你永远不会知道你的测试代码是否正确。这就是为什么它应该尽可能简单。它也应该在你修复代码之前执行 - 这样你可以避免当你认为你的代码是固定的而实际上测试通过因为它是错误的。

      【讨论】:

        【解决方案4】:

        很酷的问题! :)

        我编写的每个单元测试都遵循这个结构:

        // Given
         setup for the test
        
        // When
        what i specifically test
        
        // Then
        my assertions
        

        在您的示例中,我假设尝试之前是设置。你需要打电话来准备考试,没关系。

        但是如果你做好你的工作,a();和 b();在别处测试。因此,如果其中一种方法存在错误,其他测试,他们的测试,将会中断。这就是你如何区分它中断的地方。

        Given 和 Then 永远不会中断。您正在测试何时。

        如果您不确定 a() 和 b(),请模拟它们。

        最后一种情况,如果您想区分异常,请断言消息。

        【讨论】:

          【解决方案5】:

          只是不要捕获异常,而是将它们从您的测试方法中抛出。 JUnit 将指示测试失败和测试错误之间的区别:

          public void test() throws MyException {
              boolean result = instance.doStuff();
              Assert.true(result);
          }
          

          当您期望出现异常时,请执行以下操作:

          public void test() {
              try {
                  instance.doStuff();
                  Assert.fail("Expected MyException");
              }
              catch(MyException e) {
                  // expected
              }
          }
          

          【讨论】:

          • 您的预期异常方法有点过时,使用 @Test(expected = SomeException.class) 预期异常,它的代码更少,更不容易出错——OP 使用的是 JUnit 4。
          猜你喜欢
          • 1970-01-01
          • 2017-09-10
          • 2023-01-31
          • 1970-01-01
          • 2018-06-05
          • 1970-01-01
          • 2013-07-16
          • 1970-01-01
          • 2014-05-08
          相关资源
          最近更新 更多