单元测试的详细程度

时间:2009-12-28 04:48:43

标签: unit-testing tdd junit nunit

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

您是否测试了主要功能,包括多个方法一次执行一项任务一次? 或者你甚至可以测试自动属性?

因为,例如,我认为编写仅测试此测试的测试没什么价值:

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

因为它非常清楚,这只是浪费时间。 通常当我进行单元测试时,我会测试整个任务 - 就像这个例子一样 - 整个注册过程。

我之所以这样问是因为,目前我正在阅读Jimmy Nilsson撰写的“应用领域驱动设计和模式”一书,并指出他正在通过专门的测试来测试这些小细节。

这种覆盖水平是否过度使用?

7 个答案:

答案 0 :(得分:12)

测试不仅仅是为了测试你所写的作品。测试也可用于测试您编写的STILL的工作原理。比如,多年以及许多开发人员以后。你认为一个死的简单类可能会在未来变得更加复杂。

例如,假设它从没有过滤器开始。只是一个简单的获取/设置。为什么测试呢?然后,其他一些开发人员在正则表达式过滤器中添加了它的错误。然后突然班级被打破了,但是没有经过考验,所以没有人知道。这显示出调用堆栈进一步上升的神秘失败现在需要更多时间来调试。可能比编写一些测试所花费的更多。

然后,在将来,有人会尝试变得聪明并优化您的代码。或者重新格式化,正则表达式有一种不可读的倾向,并且通常可以进行一些清理。这对于微妙的破损是成熟的。小目标单元测试将抓住这一点。

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

测试还允许您锻炼界面并发现漏洞。当你输入不是电子邮件地址的东西时会发生什么?没错,没事。 Email.set默默地抛弃输入。垃圾进去,没什么不礼貌的。也许它应该抛出异常。当你试图测试它时,这会很快变得清晰,因为有必要测试这个集合是否有效。

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

最后,您希望将测试分离。在不受其他复杂性影响的情况下测试一件事,这样你就可以清楚地看到问题的根源。您的电子邮件课程是进行简单的解耦单元测试的绝佳选择。

答案 1 :(得分:6)

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

现在,您在上面输入的函数包含一个正则表达式,您可以将其有用地注释掉。正则表达式很难预测。与有效电子邮件地址匹配的正则表达式可能非常复杂。所以我会测试它的地狱。在单元测试后,我会进行单元测试,使用我能想到的所有不同电子邮件地址变体的正则表达式,以及所有角落和负面情况。

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

答案 2 :(得分:2)

我尝试测试任何可能引发异常的内容,或者可能以任何其他方式失败。

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

更重要的是,我使用有效的电子邮件地址调用了电子邮件,因此email具有值。然后使用无效的电子邮件地址调用它。预期的行为应该是什么?原始电子邮件地址是否仍然存在(我认为是这样),这是正确的行为吗?

如果有关于它应该如何表现的规范,那么就为单位测试IMO设定了理由。

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

答案 3 :(得分:1)

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

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

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

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

答案 4 :(得分:1)

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

正如克莱特斯所说,测试流程通常被认为是系统或集成测试。预计集成测试不会指出究竟出了什么问题,但更多的是出了问题。

答案 5 :(得分:1)

我肯定会测试你展示的代码段。它可能只是一个if语句,但正则表达式很容易出错。我使用数据驱动的测试来验证正则表达式是否正确。

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

此外,使用AAA模式编写测试(安排,执行,断言)可以帮助您理智地检查您的API,因为您的测试正在使用它!我经常测试代码中看起来很简单的部分,然后从另一个角度(用户的角度)实现,API可以更清晰。

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

答案 6 :(得分:1)

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

  1. 测试可能调用该属性的所有可能正确值。这主要是为了建立这个属性所期望的价值的完整画面。这对于后来的重构(即回归)将变得非常宝贵。
  2. 测试该属性是否返回先前成功设置的值。因为这不是直接的get / set属性,所以你要确保它返回正确的值。
  3. 测试它在传入无效值时抛出异常。由于此特定位代码确实应该处理无效输入,如果传入的值无效,则需要抛出异常
  4. 测试这个特定的原则肯定不是浪费时间,因为电子邮件验证是not all that straight forward,你应该真正测试这个中的bollocks。