有太多的单元测试吗?


139

我的任务是为现有应用程序编写单元测试。完成第一个文件后,我有717行测试代码和419行原始代码。

随着我们增加代码覆盖率,这个比率会变得难以管理吗?

我对单元测试的理解是测试类中的每个方法,以确保每个方法都能按预期工作。但是,在请求请求中,我的技术负责人指出我应该专注于更高级别的测试。他建议测试该类最常使用的4-5个用例,而不是详尽地测试每个功能。

我相信我的技术主管的评论。他比我拥有更多的经验,并且在设计软件时具有更好的直觉。但是,多人团队如何针对这种含糊的标准编写测试;也就是说,我怎么知道我的同龄人和我对“最常见的用例”有相同的想法?

对我来说,100%的单元测试覆盖率是一个崇高的目标,但是即使我们只达到50%,我们也知道那50%的覆盖率达到了100%。否则,为每个文件的一部分编写测试将留出很大的作弊空间。


145
这取决于。您是在打井字游戏,还是在编写代码来管理核反应堆?
布莱恩·奥克利

11
有了足够多的单元测试,您就可以检测到异乎寻常的硬件实现问题,例如Pentium FDIV错误或加密原语中的相关性,因此似乎没有一个硬性限制,以后再进行任何单元测试都不会有用。仅在价格昂贵时实际限制。
纳特

5
更高级别的测试将为您提供实际覆盖范围的更好视角。实际覆盖范围是指在系统的常规使用期间更可能发生这种情况。这就是您要首先实现的覆盖范围。在最后达到的50%中,YAGNI或无效代码一旦被删除也将有助于增加总体覆盖率。
拉夫

5
如果测试过多(目前似乎还没有),则最有可能的问题是所测试的代码执行过多。因此,不遵守单一责任。如果对代码进行了很好的划分,则测试也不会造成太大的负担。如果上课太多,会有很多副作用等,那将成为一场噩梦。
卢克·弗兰肯

12
sqlite测试文档非常有趣:sqlite.org/testing.html。Quote:“ SQLite库包含大约122.9 KSLOC的C代码。相比之下,该项目具有745倍的测试代码和测试脚本-91596.1 KSLOC。”
user60561

Answers:


180

是的,覆盖率100%,您将编写一些不需要的测试。不幸的是,确定不需要哪些测试的唯一可靠方法是编写所有测试,然后等待10年左右,看看哪些测试从未失败。

维护大量测试通常没有问题。许多团队在100%单元测试覆盖率之上还具有自动集成和系统测试功能。

但是,您还没有处于测试维护阶段,而是在追赶。在50%的测试覆盖率下让100%的班级比在100%的测试覆盖率下让50%的班级要好得多,而且您的潜在客户似乎正试图让您相应地分配时间。有了该基准后,下一步通常是将100%的文件向前推送。


11
感谢您的回答。这有助于我理解我的问题并解决了真正的问题-我的态度!+1
user2954463

43
@astra你的态度还不错。质疑为什么很好。要回答另一个问题,一个很好的问题是:“我怎么知道我的同龄人和我对“最常见的用例”有相同的想法?您可以让他们查看您的测试。查看他们的测试。谈论它们。您将学习代码审查测试很少浪费时间,尽管我倾向于在码头而不是会议室做我的工作
candied_orange

18
十年内从未失败的测试甚至无法保证它是不必要的,它可能会在11
。– Pharap

24
务实地,您可以采取相反的方法。编写您认为涵盖常见情况的测试。但是,每当您遇到故障时,都要编写测试覆盖该区域。
stannius

10
@Pharap这个答案的唯一问题是隐含的假设,即测试仅在失败时才能增加价值。良好的单元测试还提供了丰富的生活文档。当您编写测试时,它还迫使您考虑可重用性/可组合性/封装性,从而增加了价值。根据我的经验,未经测试的代码往往是不灵活的整体野兽。
ArTs

66

如果您使用的是使用“测试驱动开发”创建的大型代码库,那么您已经知道可能存在太多的单元测试。在某些情况下,大多数开发工作都包括更新低质量测试,最好在运行时将其作为相关类中的不变,前提和后置条件检查来实现(即,作为高级测试的副作用) )。

另一个问题是,使用基于货物的驱动设计技术来创建质量低下的设计,这会导致要测试的事物数量激增(更多的类,接口等)。在这种情况下,负担似乎在更新测试代码,但真正的问题是质量差的设计。


16
为了指出先决条件,后继条件和不变式而提出的建议应视为单元测试。这样,当该调试代码处于活动状态时,每次使用都是一次单元测试。
Persixty'5

这是一个很好的答案,完全符合我的经验。
托尼·恩尼斯

还有一个问题:如果您签入的登机手续(您确实应该这样做!)具有大量的低质量,那么即使长时间运行的测试也可能使所有功能变慢,而没有提供任何真正的好处。然后很明显,有趣的事实是,您在类中更改一件事,并且数百个测试失败。
Voo

3
这是一个比公认的更好的答案!“在某些情况下,大多数开发工作都包括更新低质量的测试”-我已经经历过,这很糟糕。在某些方面,完全没有测试。
本杰明·霍奇森

36

您问题的答案

有太多的单元测试吗?

当然...例如,您可能有多个测试,乍看之下似乎并不相同,但实际上测试的是相同的东西(逻辑上取决于被测试的“有趣”应用程序代码的相同行)。

或者,您可以测试代码的内部结构,这些内部结构永远不会向外浮出水面(即,它不是任何类型的接口协定的一部分),在那里人们可能会争论是否完全有意义。例如,内部日志消息的确切措辞或其他内容。

我的任务是为现有应用程序编写单元测试。完成第一个文件后,我有717行测试代码和419行原始代码。

这让我感到很正常。您的测试会在实际测试的基础上花费大量代码来进行设置和拆卸。该比率可能会提高,也可能不会。我本人的测试相当繁重,并且经常在测试上花费比实际代码更多的时间和精力。

随着我们增加代码覆盖率,这个比率会变得难以管理吗?

该比率没有考虑太多。测试还有其他一些性质,往往使其难以管理。如果在对代码进行相当简单的更改时经常需要重构大量测试,则应仔细查看原因。这些不是您拥有多少行,而是您如何进行测试编码。

我对单元测试的理解是测试类中的每个方法,以确保每个方法都能按预期工作。

从严格意义上讲,这对于“单元”测试是正确的。在这里,“单位”类似于方法或类。“单元”测试的重点是仅测试一个特定的代码单元,而不是整个系统。理想情况下,您将删除系统的整个其余部分(使用double或诸如此类)。

但是,在请求请求中,我的技术负责人指出我应该专注于更高级别的测试。

然后,当人们单元测试时,他们就陷入了假设人们实际上意味着单元测试的陷阱。我遇到了许多程序员,他们说“单元测试”,但含义完全不同。

他建议测试该类最常使用的4-5个用例,而不是详尽地测试每个功能。

当然,只专注于重要代码的前80%也会减少工作量……我感谢您对老板的评价很高,但这并不是我的最佳选择。

对我来说,100%的单元测试覆盖率是一个崇高的目标,但是即使我们只达到50%,我们也知道那50%的覆盖率达到了100%。

我不知道什么是“单元测试范围”。我假设您的意思是“代码覆盖率”,即运行测试套件后,每行代码(= 100%)至少执行了一次。

这是一个不错的标准,但到目前为止,还没有达到最佳标准。仅仅执行代码行并不是全部。例如,这并不说明通过复杂的嵌套分支的不同路径。它更像是一种指标,将其手指指向测试过少的代码段(很明显,如果一个类的代码覆盖率为10%或5%,则出问题了);另一方面,100%的覆盖率不会告诉您您是否已经测试足够或测试是否正确。

整合测试

默认情况下,当人们今天不断谈论单元测试时,这使我非常恼火。我认为(和经验),单元测试对于库/ API非常有用;在更多面向业务的领域(我们在此处讨论用例,例如眼前的问题),它们不一定是最佳选择。

对于一般应用程序代码和一般业务(在这方面,赚钱,按时完成任务和提高客户满意度很重要,并且您主要想避免直接出现在用户面前的错误或可能导致真正灾难的错误-我们不是这里所说NASA火箭发射),整合或功能测试是很多更加有用。

它们与行为驱动开发或功能驱动开发并驾齐驱。根据定义,那些不适用于(严格)单元测试。

为了简短起见,集成/功能测试会遍历整个应用程序堆栈。在基于Web的应用程序,它会像一个浏览器通过点击应用程序(也没有,显然不具备成为该简单化,有非常强大的框架存在,要做到这一点-退房的http://黄瓜。 io)。

哦,回答您的最后一个问题:通过确保仅在新功能实施和失败后才对其进行编程,才能使整个团队具有较高的测试覆盖率。是的,这意味着所有功能。这样可以保证 100%(正)的功能覆盖率。根据定义,它可以保证您应用程序的功能永远不会“消失”。它不能保证100%的代码覆盖率(例如,除非您主动编程否定功能,否则您将不会执行错误处理/异常处理)。

它不能保证您没有错误的应用程序。当然,您会希望针对明显或非常危险的错误情况,错误的用户输入,黑客行为(例如,周围的会话管理,安全性等)编写功能测试;但是,即使仅对肯定的测试进行编程也具有巨大的好处,并且对于现代,强大的框架而言是完全可行的。

功能/集成测试显然具有其自身的蠕虫能力(例如,性能;对第三方框架的冗余测试;由于您通常不使用double,因此根据我的经验,它们也往往更难编写...),但是我d每天都要使用100%经过正面功能测试的应用程序,而不是100%经过代码覆盖率单位测试的应用程序(不是库!)。


1
集成测试很棒,但不能替代单元测试,也不能替代业务应用程序。它们有多个问题:a)按照定义,它们需要很长时间才能运行(这也意味着增量测试几乎没有用),b)它们使查明实际问题变得异常困难(哦,50个集成测试刚刚失败,是什么变化导致的?)和c)它们反复覆盖相同的代码路径。
Voo

1
a)是一个问题,因为它使在门控的签入程序上运行测试变得麻烦,并且使开发人员在开发过程中重复运行测试的可能性降低,再加上b)降低了效率和快速诊断错误的能力。c)意味着更改一件小事很容易导致数十个或数百个集成测试失败,这意味着您将花费大量时间修复它们。这也意味着集成测试主要只测试快乐的路径,因为针对这些目标编写这些测试既麻烦又不可能。
Voo

1
@Voo,您所写的所有内容都是真实的,据我所知,我已经提到您在答案中指出的所有问题……
AnoE

如果您同意该总结,那么我真的看不到如何得出结论,您更喜欢集成测试而不是单元测试。大型程序的综合集成测试套件需要花费数小时甚至数天才能运行,它们虽然很棒,但在实际开发中几乎没有用。而且您的验收测试(每个人都在做,对吗?)将遇到许多集成测试发现会被单元测试遗漏的相同问题-相反,事实并非如此。
Voo

24

是的,可能有太多的单元测试。例如,如果您具有100%的单元测试覆盖率,而没有集成测试,那么您将遇到明显的问题。

一些方案:

  1. 您将测试过度设计为特定的实现。然后,您必须在重构时放弃单元测试,而不是在更改实现时放弃(执行性能优化时经常遇到的痛点)。

    在单元测试和集成测试之间取得良好的平衡可以减少此问题,而又不会损失很多覆盖范围。

  2. 您可以使用每次测试的20%对每次提交进行合理的覆盖,而将其余80%用于集成或至少单独的测试通过。在这种情况下,您看到的主要负面影响是更改缓慢,因为您必须等待大量时间才能执行测试。

  3. 您修改了太多代码,无法对其进行测试。例如,我已经看到很多IoC在不需要修改的组件上的滥用,或者至少对它们进行泛化花费高昂且优先级低,但是人们花了很多时间对它们进行泛化和重构以进行单元测试。

我特别同意关于100%文件覆盖率达到50%而不是50%文件覆盖率达到100%的建议。将您的最初工作重点放在最常见的积极案例和最危险的消极案例上,不要在错误处理和异常路径上投入过多,不是因为它们并不重要,而是因为您的时间有限且测试范围无限,因此,您需要优先考虑任何情况。


2
这对单元测试不是问题,但是对于组织而言,由于要求特定数量的单元测试覆盖范围而没有花费资源来在其他级别上创建和执行适当的测试,因此其优先级错了。
jwenting

2
强烈同意#3,并且还将其扩展为将较低级别的类的实例手动传递给较高级别的类。如果高级事物依靠一些低级事物来完成某件事,那没关系。如果它对调用者隐藏了详细信息,我将称其为良好的设计。但是,如果您随后将低级事物作为高级事物的接口的一部分并让呼叫者通过它,因为它使您的测试漂亮,那么尾巴就会摇动它。(如果低级别的东西在很多地方都可以重复使用,并且发生了很多变化,那将会改变事情。根据我的经验,这不是典型的。)
johncip

我喜欢您的描述@johncip,绝对是一个常见的例子,说明如何通过向构造函数添加一些不必要的必需参数来使一个好的类变得可怕...
Bruno Guardia

19

请记住,每个测试既有成本,也有好处。缺点包括:

  • 必须编写测试;
  • 测试需要(通常很少)时间来运行;
  • 必须使用代码维护测试-当测试API时,测试必须更改;
  • 您可能必须更改设计才能编写测试(尽管这些更改通常会更好)。

如果成本超过收益,则最好不要编写测试。例如,如果功能很难测试,API经常更改,正确性相对不重要,并且测试发现缺陷的机会很低,那么最好不编写它。

至于测试与代码的特定比率,如果代码具有足够的逻辑密度,则可以保证该比率。但是,在整个典型应用程序中保持如此高的比率可能不值得。


12

是的,有太多的单元测试。

虽然测试很好,但是每个单元测试都是:

  • 与API紧密相关的潜在维护负担

  • 可以花在别的事情上的时间

  • 单元测试套件中的一小段时间
  • 可能没有添加任何实际价值,因为它实际上是某些其他测试的重复项,而其他某些测试将通过的可能性很小,而该测试将失败。

争取100%的代码覆盖率是明智的,但这远非意味着一组测试,每个测试都可以在某些指定的入口点(功能/方法/调用等)独立地提供100%的代码覆盖率。

尽管考虑到要获得良好的覆盖率和驱除错误有多困难,但事实可能是“错误的单元测试”和“太多的单元测试”之类的东西。

大多数代码的语用表明:

  1. 确保您具有100%的入口点覆盖率(所有内容均已通过某种方式测试),并力争接近“非错误”路径的100%代码覆盖率。

  2. 测试任何相关的最小/最大值或大小

  3. 测试任何您认为有趣的特例,尤其是“奇数”值。

  4. 当您发现一个错误时,添加一个可以揭示该错误的单元测试,并考虑是否应该添加任何类似的案例。

对于更复杂的算法,还请考虑:

  1. 对更多案例进行一些批量测试。
  2. 将结果与“强力”实现进行比较,并检查不变量。
  3. 使用一些产生随机测试用例并检查暴力和后置条件(包括不变量)的方法。

例如,检查具有一些随机输入的排序算法,并通过扫描对数据进行最后验证以对数据进行排序。

我想说您的技术负责人正在提议“最小裸眼”测试。我正在提供“最高价值的质量测试”,两者之间存在着一定的距离。

也许您的前辈知道您要构建的组件将被嵌入到更大的部件中,并且在集成时将对其进行更彻底的单元测试。

关键课是在发现错误时添加测试。这给了我关于开发单元测试的最好的教训:

关注单位而不是子单位。如果要从子单元中构建单元,请为子单元编写非常基本的测试,直到它们变得合理为止,并通过控制单元对子单元进行测试来获得更好的覆盖率。

因此,如果您正在编写编译器,并且需要编写符号表(例如)。使用基本测试启动并运行符号表,然后继续(说)填充表的声明解析器。仅当在符号表“独立”单元中发现错误时,才对其进行进一步测试。否则,在声明解析器以及整个编译器上通过单元测试来增加覆盖范围。

这是最划算的选择(整体测试之一是测试多个组件),并留出更多的空间进行重新设计和优化,因为在测试中仅使用了“外部”接口,这往往更加稳定。

结合调试代码测试的前提条件,后条件(包括各个级别的不变式),您可以通过最少的测试实现获得最大的测试覆盖率。


4
我不会说100%的覆盖率是务实的。100%的覆盖率是一个很高的标准。
Bryan Oakley

不幸的是,即使是随机方法也会遗漏错误。即使非正式,也无法替代证明。
Frank Hileman '17

@BryanOakley点已采取。这是一个夸大的说法。但是,接近人们比给予别人信任更重要。“我测试了简单的方法,这一切都很好”总是会在以后引起问题。
Persixty》,

@FrankHileman问题不是“单元测试是否可以替代仔细设计软件,静态检查逻辑和证明算法”,那么答案是否定的。两种方法都不能自行产生高质量的软件。
Persixty

3

首先,拥有比生产代码更多的测试不一定是问题。测试代码是线性的(或应该是线性的),并且易于理解-无论生产代码是否正确,测试代码的必要复杂度非常非常低。如果测试的复杂性开始接近生产代码的复杂性,那么您可能确实有问题。

是的,可能会有太多的单元测试-一个简单的思想实验表明,您可以继续添加不提供附加价值的测试,并且所有这些添加的测试至少可以抑制某些重构。

我认为,仅测试最常见案例的建议是有缺陷的。这些可以作为冒烟测试以节省系统测试时间,但是真正有价值的测试可以捕获整个系统中难以执行的案例。例如,内存分配故障的受控错误注入可用于执行恢复路径,否则恢复路径的质量可能完全未知。或传递零作为您知道的将被用作除数的值(或负数将平方根),并确保您不会遇到未处理的异常。

接下来最有价值的测试是那些行使极限或边界点的测试。例如,接受一年中(以1为基)月的函数应使用0、1、12和13进行测试,因此您知道有效无效转换在正确的位置。对于这些测试也使用2..11是过度测试。

您处于困境,因为您必须为现有代码编写测试。在编写(或即将编写)代码时,更容易识别出极端情况。


3

我对单元测试的理解是测试类中的每个方法,以确保每个方法都能按预期工作。

这种理解是错误的。

单元测试验证行为的的测试单元

从这个意义上讲,单位不一定是“类中的方法”。我喜欢Roy Osherove在“单元测试的艺术”中对单元的定义:

单位是具有相同更改原因的所有生产代码。

基于此,单元测试应验证代码的每个所需行为。“欲望”或多或少地来自需求。


但是,在请求请求中,我的技术负责人指出我应该专注于更高级别的测试。

他是对的,但是与他想像的方式不同。

根据您的问题,我知道您是该项目中的“专用测试人员”。

最大的误解是他希望您编写单元测试(与“使用单元测试框架进行测试”相反)。编写ynit测试是开发人员责任,而不是测试人员的责任(在理想的世界中,我知道...)。另一方面,您用TDD标记了这个问题,这恰恰暗示了这一点。

作为测试人员,您的工作是编写(或手动执行)模块和/或应用程序测试。并且这种测试应主要验证所有单元是否可以顺利协作。这意味着您必须选择测试用例,以便每个单元至少执行一次。而检查是运行。实际结果不太重要,因为它可能会随将来的要求而变化。

再一次强调自卸汽车的类比:在装配线末端对一辆汽车进行了多少次测试?恰好一个:它必须自己开车去停车场...

这里的重点是:

我们需要意识到“单元测试”和“使用单元测试框架进行自动化测试”之间的区别。


对我来说,100%的单元测试覆盖率是一个崇高的目标,但是即使我们只达到50%,我们也知道那50%的覆盖率达到了100%。

单元测试是一个安全网。它们使您有信心重构代码,以减少技术负担或增加新行为,而不必担心破坏已实施的行为。

您不需要100%的代码覆盖率。

但是您需要100%的行为覆盖率。(是的,代码覆盖率和行为覆盖率以某种方式相互关联,但是出于此原因它们并不相同。)

如果您的行为覆盖率不到100%,那么成功运行测试套件就没有任何意义,因为您可以更改一些未经测试的行为。发布发布第二天,您就会受到客户的关注...


结论

很少有测试比没有测试更好。毫无疑问!

但是,没有太多的单元测试。

这是因为每个单元测试都会验证对代码行为单一期望。而且,您编写的单元测试不能超出对代码的期望。安全带上的孔可能会造成意外更改,从而损害生产系统。


2

绝对没错。我曾经是一家大型软件公司的SDET。我们的小型团队必须维护以前由大型团队处理的测试代码。最重要的是,我们的产品具有一些依赖关系,这些依赖关系不断带来重大变化,这意味着我们需要不断进行测试维护。我们没有选择增加团队规模的选择,因此当它们失败时,我们不得不扔掉数千个价值不高的测试。否则,我们将永远无法跟上缺陷。

在您将此视为单纯的管理问题之前,请考虑一下现实世界中的许多项目在接近传统状态时会遭受人员减少的困扰。有时它甚至在第一个发行版之后就开始发生。


4
“最重要的是,我们的产品具有某些依赖关系,这些依赖关系不断带来重大变化,这意味着我们需要不断进行测试维护。” -如果您的依赖关系不断中断,您说的那些测试要求维护听起来像是有价值的测试。
CodeMonkey '17年

2
这不是测试的问题,而是组织的问题。
jwenting

2
@CodeMonkey依赖关系没有中断。它们以需要更改我们产品的方式进行更新。是的,测试是有价值的,但远没有其他有价值。当等效的手动测试很困难时,自动测试最有价值。
mrog

2
@jwenting是的,这是组织问题,而不是代码问题。但这并不能改变测试太多的事实。无论原因如何,无法调查的失败测试都是没有用的。
mrog

什么是“ SDET”?
彼得·莫滕森

1

假设您要重构测试代码以消除复制粘贴,那么拥有比产品代码多的测试代码行并不一定是问题。

问题在于,测试是实现的镜像,没有任何业务意义-例如,加载了模拟和存根并且仅断言某个方法调用其他方法的测试。

“为什么大多数单元测试是浪费的”一文中,一个引人注目的是单元测试应该具有“广泛,正式,独立的正确性和可确定的业务价值”


0

我没有看到的一件事是您的测试需要快速且容易地进行,以使任何开发人员都可以随时运行。

您不需要在测试完成之前先检查源代码控制并等待一个小时或更长时间(取决于代码库的大小)来查看您的更改是否破坏了某些东西-您希望能够做到这一点在签入源代码管理之前(或至少在您进行更改之前)使用自己的计算机。理想情况下,您应该能够通过单个脚本或按钮按下来运行测试。

而且,当您在本地运行这些测试时,您希望它们能够快速运行-数秒之内。速度慢一点,您会很想没有足够或根本没有运行它们。

因此,进行如此多的测试以至于全部运行都需要几分钟,或者进行了一些过于复杂的测试,可能会成为问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.