【问题标题】:Level of detail of your unit tests [closed]单元测试的详细程度[关闭]
【发布时间】:2009-12-28 04:48:42
【问题描述】:

我想开始讨论您在单元测试中涵盖的细节。

您是否对主要功能进行测试,这些功能包括通过一次测试同时完成一项任务的多种方法? 或者您甚至可以测试自动属性?

因为,例如,我认为编写一个只测试这个的测试没有什么价值:

   public Email
   {
      set
      {
         if(Regex.Match(/*....*/))
             email = value;
      }
      get
      {
        return email;
      }
   }

因为它真的很清楚,这只是浪费时间。 通常,当我进行单元测试时,我会测试一个完整的任务——比如这个例子——一个完整的注册过程。

我之所以问这个问题是因为,目前我正在阅读 Jimmy Nilsson 所著的《应用领域驱动设计和模式》一书,他指出他正在使用专门的测试来测试这些小细节。

这样的覆盖率不是过度使用吗?

【问题讨论】:

  • 您需要多少单元测试覆盖率? artima.com/weblogs/viewpost.jsp?thread=204677
  • 不认为你需要测试一个电子邮件类?继续,给我看你的电子邮件正则表达式,我会给你一个有效的地址来破坏它。

标签: unit-testing tdd junit nunit


【解决方案1】:

测试不仅仅是为了测试你写的东西是否有效。还可以进行测试以测试您编写的内容是否仍然有效。就像,许多年后和许多开发人员一样。你认为一个死的简单的类在未来可能会变得更加复杂。

例如,假设它开始时没有过滤器。只是一个简单的获取/设置。为什么要这样测试?然后后来一些其他开发人员添加了正则表达式过滤器,并且它有问题。然后突然班级被打破了,但它未经测试所以没有人知道。这表明调用堆栈中出现了神秘的故障,现在需要更多时间来调试。可能比编写几个测试所花费的更多。

然后,在未来,有人会尝试变得聪明并优化您的代码。或者重新格式化它,正则表达式倾向于写成不可读的并且通常可以进行一些清理。这对于微妙的破坏已经成熟。小型目标单元测试会捕捉到这一点。

在您上面的示例中,正则表达式可能用于过滤看起来像电子邮件地址的内容。这需要检查正则表达式是否正常工作,否则您的电子邮件类将停止接受电子邮件。或者它开始胡言乱语。也许您的正则表达式没有涵盖有效的电子邮件地址,一旦您发现它就值得测试。最终,有人会用实际的解析器替换你的正则表达式。也许它有效,也许它没有。一个好的测试将有一个简单的有效和无效电子邮件地址列表,您可以在发现极端情况时轻松添加这些地址。

测试还可以让您练习您的界面并发现漏洞。当您输入不是电子邮件地址的内容时会发生什么?没错,什么都没有。 Email.set 默默地丢弃输入。垃圾进了,没出是很不礼貌的。也许它应该抛出一个异常。当您尝试对其进行测试时,这一点很快就会变得清晰,因为有必要测试该集合是否有效。

测试还可以揭示不灵活和无法覆盖或自定义的东西。在您的示例中,直接测试正则表达式过滤器会很方便,而不必每次都实例化一个对象。这是因为过滤器是最复杂的部分,它更容易测试和调试,同时通过尽可能少的层。通过将其放入Email.is_email_address,您现在可以直接对其进行测试。作为副作用,它也可以在子类中被覆盖。这很方便,因为大多数人都会收到电子邮件验证错误,因为EMAIL HATES THE LIVING!

最后,您希望将测试解耦。在不受其他复杂性影响的情况下测试一件事,这样您就可以清楚地看到问题的根源。您的 Email 类非常适合进行简单的解耦单元测试。

【讨论】:

    【解决方案2】:

    简单函数的测试本身很简单。因此它们易于编写且易于运行。它们只需要很少的时间来创建,并且是以后模块变得更复杂时的占位符。所以,是的,甚至测试简单的功能。

    现在,您在上面键入的函数包含一个正则表达式,您将其注释掉了。正则表达式是出了名的难以预测。匹配有效电子邮件地址的正则表达式可能非常复杂。所以我会测试它的地狱。我会在单元测试之后进行单元测试,将该正则表达式与我能想到的所有不同的电子邮件地址变体以及所有角落和负面案例相结合。

    关于是否编写测试的决定始终是关于短期和长期利益的决定。从长远来看,测试总是有益的,因为它们会锚定代码的行为并检测副作用和意外后果。推迟考试的行为总是为了短期利益,这样你就不会“浪费时间”。问题是,您不编写的任何测试都是系统中的一个漏洞,其中副作用和意外后果可能会累积而未被发现。由于简单模块的测试快速且易于编写,并且墨菲将确保讨厌的错误将隐藏在您为它们提供的任何漏洞中,这些简单的测试似乎不太可能真正浪费时间。

    【讨论】:

      【解决方案3】:

      我尝试测试任何可能引发异常或可能以任何其他方式失败的东西。

      例如,在您的示例中,如果有人输入了不是电子邮件地址的内容,您会默默地失败吗?

      更重要的是,我用一个有效的电子邮件地址调用了电子邮件,所以email 有一个值。然后它被一个无效的电子邮件地址调用。预期的行为应该是什么?原来的电子邮件地址还在吗(我想是的),这是正确的行为吗?

      如果有关于它应该如何表现的规范,那么这就为单元测试奠定了基础,IMO。

      如果您在更高级别测试此函数,那么您只需要确保您正在测试此 setter 的所有可能情况,但测试 getter 部分是没有意义的。

      【讨论】:

      • 在我的示例中,即使您在无效电子邮件上抛出异常,该行为仍然非常可预测,我怀疑测试这么小的一段代码的意义。如果您看一下 ADDD&P 的第 80-84 页,您会注意到即使是这样的细节也经过了测试——就像编写一个完整的测试方法来测试两个整数的加法一样。
      • +1 用于测试完整性检查 API 设计
      • @Karim - 我倾向于测试任何可能有错误或意外响应的东西。例如,如果您总是假设添加两个整数是正数,那么如果它是负数,您应该正确处理,因此,需要进行测试。但是,如果你不在乎正面、负面、溢出,那就没必要测试了。
      • @Karim - 如果在您的示例中抛出异常,则该值不会更改,因此您的属性中仍然有一个有效的电子邮件地址,除非您之前设置了 'email=""'使用正则表达式。但是,无论如何,应该有一些文件解释在无效电子邮件地址的情况下应该是什么状态。那将是测试用例。
      【解决方案4】:

      有人可能会争辩说,测试整个注册过程太高级了。小块功能往往更容易编写测试,而更复杂的测试通常需要相当多的脚手架。

      严格的方法是在单元测试中实现 100%(或接近)的代码覆盖率(许多 IDE 要么能够直接测量这一点,要么通过某种插件进行测量)。在实践中,您可能不想这么严格,而是选择您需要工作的关键类,因为还有很多其他事情取决于它们。

      dependency injection(“DI”)或inversion of control(“IoC”)的优点之一是它鼓励您将代码分解为可插入的部分,这不仅使您的代码更易于测试(因为您可以轻松插入模拟)但也倾向于使这些片段更小。为许多小块编写测试往往比为较少的大块编写测试要快得多。

      测试整个注册过程更多是关于系统或集成测试。

      【讨论】:

        【解决方案5】:

        unit 测试的一个重要属性是它们在失败时清楚地显示出了什么问题。如果他们测试非常小的功能,这是可能的。例如,如果您测试整个注册过程,您无法确定哪一行出错了。

        正如 cletus 所说,测试流程通常被视为系统或集成测试。集成测试不应准确指出问题所在,而更多的是指出问题所在。

        【讨论】:

          【解决方案6】:

          我当然会测试你展示的代码 sn-p。它可能只是一个 if 语句,但正则表达式非常容易出错。我会使用数据驱动的测试来验证正则表达式是否正确。

          即使只是if(some simple condition) 的情况,我也会提倡编写测试以确保它是正确的。毕竟,如果逻辑很简单,你可能会在写的时候关掉你的大脑。同样,很容易得到一个!在错误的地方,使用 && 而不是 ||,在错误的地方添加括号或者只是犯一个基本错误。如果您在在编写新代码之前(新代码)添加一个测试来覆盖新代码,那么测试通常会在您继续之前发现错误。

          此外,使用 AAA 模式(排列、动作、断言)编写测试有助于您全面检查 API,因为您的测试正在使用它!我经常测试我的代码中看起来很简单的部分,然后意识到,从另一个角度(用户的角度),API 可以更清晰。

          我个人不会使用专门的测试来测试自动属性,除非我知道实现可能会改变。这段时间最好花在添加更多覆盖潜在边缘情况的测试上,或者为代码的最高流量区域添加更多测试。

          【讨论】:

            【解决方案7】:

            您应该测试此特定属性的原因有很多。以下是不同的测试及其背后的原因。

            1. 测试所有可能的正确值,该属性可能被调用。这主要是为了全面了解该物业所期望的价值。这对于以后的重构(即回归)将变得非常宝贵。
            2. 测试该属性是否返回先前成功设置的值。因为这不是直接的 get/set 属性,所以您要确保它返回正确的值。
            3. 测试是否在传入 invalid 值时抛出异常。由于这段特定的代码确实应该处理无效输入,因此如果传入的值无效,则需要引发异常.

            测试这个特定的属性肯定不是浪费时间,因为电子邮件验证是 not all that straight forward ,你真的应该测试一下这个废话。

            【讨论】:

              最近更新 更多