有没有所谓的过度单元测试?

17

我对单元测试的概念并不陌生,但同时我也没有完全掌握。

最近在使用TDD方法编写代码时,我一直在思考一个问题:应该测试到什么程度?

有时候我会想,我在使用单元测试方面是否过度了。

开发人员应该在什么时候停止编写单元测试,开始实际工作呢?

在人们认为我反对使用TDD之前,我可能需要澄清这个问题...

我困扰于我的测试粒度...

  • 当我的应用程序具有配置文件时,我需要测试文件中的值是否可以被检索出来吗?我倾向于是....但是....
  • 那么我要为每个可能的配置值编写一个单元测试吗?即检查它们是否存在...并且可以解析为正确的类型...
  • 当我的应用程序将错误写入日志时,我需要测试它是否能够写入日志吗?然后我需要编写测试来验证条目是否真的已经添加到日志中吗?

我想要使用我的单元测试来验证我的应用程序的行为...但是我不确定何时停止。是否有可能编写过于琐碎的测试?

18个回答

30

[更新:] 在《测试驱动开发实战》第194页找到了这个问题的简洁答案。

Phlip提供的简单答案是: "编写测试,直至恐惧转化为无聊。"

[/更新]

我认为当前时代普遍存在的问题是缺乏单元测试...而不是过度测试。我想我知道你的意思.. 我不会把它称为过度单元测试,而是..没有在你关注的地方聚焦努力。

因此,回答你的问题..给出一些指导方针。

  • 如果你遵循TDD(测试驱动开发),你永远不会有未被单元测试覆盖的代码.. 因为你只写(最小)的代码来通过失败的单元测试,而不再多写。推论:每个问题应该都失败于一个能够准确定位缺陷位置的单元测试。同一个缺陷不应该同时导致数十个单元测试失败。
  • 不要测试你没写的代码。 推论是:你不需要测试框架代码(比如从 app.config 文件中读取值),你只需要假设它可以正常工作。而且你有多少次遇到过框架代码出错?几乎是零。
  • 如果有疑问,请考虑失败的可能性并权衡编写自动化测试用例的成本。包括编写针对访问器/重复数据集的测试用例。
  • 解决问题关键。如果你发现自己在某一特定领域定期遇到问题,请将其放入测试套件中..而不是花时间为那些你知道非常稳定的区域编写冗余的测试。例如,第三方/团队库在接口处经常出问题.. 不像它应该的那样工作。模拟无法捕获它。使用真实的协作者进行回归类型套件,并运行一些检查链接的基本测试,如果你知道它一直都是个问题儿童。

当Phlip说同样的缺陷不应该导致其他测试失败时,我的经验是这并非总是如此。例如,我前几天正在使用TDD编写一个用于解释协议的解释器,并使用模拟底层数据流一起测试每个协议命令及其响应。如果添加了一个bug以破坏,比如说解析机制,使得没有响应出现,所有这些测试都将失败。但是,它们将在每个测试的同一位置中全部失败。这算不算错了呢? - Kaz Dragon
Phlip刚说了第一句:)剩下的就是我随口说的。一个缺陷对应一个破碎的测试:在你的情况下,解释器代码中的一个缺陷应该被一个文本标记出来。我可能在这里错了...你是不是说所有命令都使用的共同代码块有一个错误?如果是这样,那么应该通过低级别测试来检查常见的代码块-从而集中于修复区域。建立在这个单元之上的测试注定会失败,这没关系。简而言之,你的测试应该告诉你缺陷在哪里;而不是你必须调试测试以找出问题所在。 - Gishu
1
有时候测试你没有编写的代码是有意义的。你可能想要测试你对代码的理解而不是代码本身。也许框架是完美无缺的,但你误解了其中一个参数的目的等等。尽管如此,你仍然希望将大部分测试分配给你自己编写的代码。 - John D. Cook
@John - 当然可以 - 我相信它被称为“学习者测试”。然而,这是此处指南的例外。学习者测试的目的是通过一些断言来验证您对未知代码的理解,不是严格意义上的单元测试或TDD...更多的是测试驱动的学习。 - Gishu
有人知道Phlip是谁吗?我一直在试图追踪这个引用的来源。《通过示例学习测试驱动开发》的致谢中提到了Phlip作为技术审查者,但没有提供全名或其他信息。 - danvk

9

确实可以编写过多的单元测试。例如:

  • 测试getter和setter。
  • 测试基本语言功能是否正常工作。

语言/平台没有被提及。但是,如果它是.NET getters和setters(以及构造函数),可以使用CodePlex的Automatic Class Tester项目轻松测试http://www.codeplex.com/classtester。 - Joseph Ferris
当然,但我认为这些测试并不能让你学到很多东西(参见https://dev59.com/CXVD5IYBdhLWcg3wDHDm),因为getter和setter也会被自动生成。不需要为生成的代码生成测试。 - Olaf Kock

8

实际上,问题不在于人们编写了太多的测试,而是它们分配测试不均。有时你会看到新手编写数百个测试来测试容易测试的事物,但是在没有将任何测试放在最需要测试的地方之前就失去了动力。


4

一定要注意不要过度测试单元,测试功能是一个好的起点。但也不要忽视测试 错误处理。当输入不满足前置条件时,你的代码应该有合理的反应。如果是你自己的代码导致了不良输入,则断言失败是合理的响应。如果用户可以导致不良输入,那么你需要对异常或错误消息进行单元测试。

每个报告的漏洞都应当至少对应一个单元测试。

关于你所提到的一些细节:我一定会测试我的配置文件解析器,确保它可以解析每个预期类型的值。 ( 我倾向于使用Lua来进行配置文件和解析,但仍然需要进行一些测试。)但我不会为配置文件中的每个条目编写单元测试;相反,我会编写一个表格驱动的测试框架,描述每个可能出现的条目,并从中生成测试。我可能会从同一描述中生成文档。我甚至可以生成解析器。

当你的应用程序将条目写入日志时,你正在进入集成测试领域。更好的方法是使用单独的日志记录组件,如syslog。然后你可以对记录器进行单元测试,将其放在架子上并重复使用它。或者更好的是,重用syslog。然后一个简短的集成测试可以告诉你你的应用程序是否与syslog正确地互操作。
一般来说,如果你发现自己写了很多单元测试,可能你的单元太大了,而且不够正交。
希望这些内容能帮到你。

3

单元测试需要测试每个功能模块,边缘情况和有时候的极端情况。

如果你发现在测试边缘和极端情况之后,你还在测试“中间”情况,那么这可能是过度了。

此外,根据你的环境,编写单元测试可能非常耗时或非常脆弱。

测试确实需要持续的维护,所以你编写的每一个测试都有可能在未来出现问题并需要修复(即使它没有检测到实际的错误) - 尝试用最少的测试进行足够的测试似乎是一个不错的目标(但不要无谓地将几个测试组合成一个 - 一次只测试一个功能)


3
我认为好的测试应该测试一些规范。任何测试超出规范范围的内容都是无用的,因此应该省略,例如测试仅用于实现单元指定功能的方法。测试真正微不足道的功能(如getter和setter)是否值得也是值得怀疑的,尽管您永远不知道它们会变得多么微不足道。
按照规范进行测试的问题在于,许多人将测试用作规范,这是错误的,原因有很多--部分原因是因为这会阻止您实际上知道应该测试什么和不应该测试什么(另一个重要原因是测试总是只测试一些示例,而规范应该始终为所有可能的输入和状态指定行为)。
如果您的单元有适当的规范(而且应该有),那么需要测试的内容应该很明显,超出这个范围的任何内容都是多余的,因此是浪费。

3
需要注意的一点是,如果你发现需要编写大量单元测试来重复执行同一项操作,那么请考虑重构相关代码的根本原因。在访问配置设置的每个位置都需要编写测试吗?不需要。如果进行重构并创建功能的单个入口点,则可以仅测试一次。我认为尽可能地测试大量功能非常重要。但是,真正重要的是要意识到,如果省略了重构步骤,随着代码库中持续出现“一次性”实现,您的代码覆盖率将急剧下降。

2

是的,单元测试可能会过度/极端化。

请记住,只有测试功能是必要的; 其他所有内容都来自于此。

因此,您不必测试是否可以从配置文件中读取值,因为一个或多个功能将需要从配置文件中读取值-如果它们没有,则不需要配置文件!

编辑:似乎有些人对我的意思感到困惑。我并不是说单元测试和功能测试是相同的-它们并不相同。根据维基百科的定义:“单元是应用程序中最小的可测试部分”,从逻辑上讲,这样的“单元”比大多数“功能”更小。

我想说的是,单元测试极端的,很少是必要的-除非是超级关键的软件(例如实时控制系统可能危及生命)或没有预算和时间限制的项目。

对于大多数软件而言,从实际角度来看,测试功能就足够了。测试比功能小的单元不会有害,而且可能有帮助,但在生产力与质量改进之间进行权衡是值得商榷的。


这听起来像是一个集成测试,如果你测试其他东西并且读取配置值。单元测试单独测试每个单元(OOP中的类),以便该单元及其单元测试可以在应用程序的任何周围代码的支持下工作。 - Mnementh
我通常在这种情况下使用一个虚拟/测试配置文件。在这种情况下,模拟配置文件是太麻烦了,我觉得。 - Gishu
1
@Mnementh:这取决于您如何定义“单元”。我将一个功能定义为唯一值得测试的软件单元。不是一个类,不是一个方法,而是一个功能。 - Steven A. Lowe
单元测试的定义是断言给定类的低级功能,而不是完整的用户功能。当然你可以按照自己的方式进行,但是当出现误解时请不要感到惊讶。 - Adam Byrtek
@[Adam Byrtek]: 请查看编辑中的澄清。 - Steven A. Lowe
@Mnementh:请查看编辑中的澄清。 - Steven A. Lowe

2

在单元测试中,您会编写一个测试,以显示可以从配置文件中读取项。您将测试任何可能的问题,以便具有代表性的一组测试,例如:您能否读取空字符串、长字符串或带转义字符的字符串,系统是否能够区分空字符串和丢失的字符串。

完成该测试后,并不需要每次另一个类使用您已经测试过的功能时都重新检查该功能是否可用。否则,对于您测试的每个函数,都必须重新测试它所依赖的每个操作系统功能。给定功能的测试仅需要测试该功能的代码应该正确执行的内容。

有时,如果这很难判断,说明需要重构以使问题更容易回答。如果您必须为不同的功能多次编写相同的测试,这可能表明这些功能之间共享某些内容,可以将其移到单个函数或类中进行测试,然后重复使用。

在更广泛的范围内,这是一个经济学问题。假设您已停止不必要的重复测试,您的测试可以有多完整?由于可能发生的情况的组合,实际上不可能为任何非微不足道的程序撰写真正完整的测试,因此您必须作出决定。许多成功的产品在最初推出时甚至没有单元测试,包括一些最著名的桌面应用程序。它们是不可靠的,但足够好了,如果它们在可靠性方面投入更多的工作,那么它们的竞争对手将在市场份额上击败它们。(看看Netscape,他们凭借一个声名狼藉的不可靠产品获得了第一名,然后当他们暂停一段时间来以正确的方式做一切事情时完全消失了)。这不是我们作为工程师想听到的,希望如今的客户更具识别能力,但我认为区别不大。


2

很有可能,但问题不在于测试过多的内容 - 而是测试你不关心的东西,或者在测试更少和更简单的测试就足够的内容时投入过多。

我的指导原则是我在更改代码时拥有的信心水平:如果它永远不会失败,我就不需要测试。如果很简单,一个简单的测试就可以了。如果很棘手,我会增加测试,直到我有信心进行更改为止。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接