您应该使用单元测试来测试什么?


122

我刚大学毕业,下周要去上大学。我们已经看到了单元测试,但是我们并没有太多地使用它们。每个人都在谈论他们,所以我想也许我应该做些。

问题是,我不知道要测试什么。我应该测试一下普通情况吗?边缘情况?我怎么知道一个功能已被充分覆盖?

我总是有一种可怕的感觉,尽管测试可以证明某个功能在特定情况下可以工作,但是证明该功能在一定时期内是完全没有用的。


看看Roy Osherove的Blog。那里有很多有关单元测试的信息,包括视频。他还写了一本书,“单元测试的艺术”,非常好。
Piers Myers

9
我想知道近五年后您对此有何看法?因为我越来越觉得现在人们应该更好地了解“不进行单元测试的内容”。行为驱动的开发已经从您提出的问题中演变而来。
RemigijusPankevičius15年

Answers:


121

到目前为止,我的个人哲学是:

  1. 测试所有可能的常见情况。这将告诉您在进行某些更改后代码何时中断(我认为这是自动化单元测试的最大好处)。
  2. 测试一些您认为可能有错误的异常复杂代码的边缘情况。
  3. 每当发现错误时,编写一个测试用例将其覆盖,然后修复
  4. 每当有人有时间杀人时,将边缘情况测试添加到不太重要的代码中。

1
多亏了这一点,我在这里苦苦挣扎,遇到了与OP相同的问题。
斯蒂芬

5
+1,尽管我还将测试任何库/实用程序类型函数的极端情况,以确保您具有逻辑API。例如,当传递null时会发生什么?空输入呢?这将有助于确保您的设计合乎逻辑,并记录极端情况下的行为。
mikera 2012年

7
#3似乎是一个非常可靠的答案,因为它是一个真实示例,说明了单元测试如何提供帮助。如果它破裂一次,它可能会再次破裂。
瑞安·格里菲斯

刚开始,我发现我在设计测试方面不是很有创造力。因此,我将它们用作上面的#3,这使您放心,这些错误将永远不会再被发现。
ankush981 '16

:你的答案被刊登这种流行的媒体文章中hackernoon.com/...
BugHunterUK

67

迄今为止,在众多答案中,没有人涉及等效划分边界值分析,这是眼前问题解答中的重要考虑因素。所有其他答案虽然很有用,但都是定性的,但有可能(最好是)在这里定量。@fishtoaster提供了一些具体的指导原则,只是在测试量化的范围内进行了偷看,但是等效划分和边界值分析使我们可以做得更好。

等价分区中,您可以根据预期结果将所有可能输入的集合分为几组。来自一组的任何输入都将产生等效的结果,因此这些组称为等效类。(请注意,同样的结果也并不意味着相同的结果。)

作为一个简单的示例,请考虑一个应将小写ASCII字符转换为大写字符的程序。其他字符应进行身份转换,即保持不变。这是对等类的一种可能分类:

| # |  Equivalence class    | Input        | Output       | # test cases |
+------------------------------------------------------------------------+
| 1 | Lowercase letter      | a - z        | A - Z        | 26           |
| 2 | Uppercase letter      | A - Z        | A - Z        | 26           |
| 3 | Non-alphabetic chars  | 0-9!@#,/"... | 0-9!@#,/"... | 42           |
| 4 | Non-printable chars   | ^C,^S,TAB... | ^C,^S,TAB... | 34           |

如果您列举所有测试用例,则最后一列将报告测试用例的数量。从技术上讲,通过@fishtoaster的规则1,您将包括52个测试用例-上面给出的前两行的所有测试用例都属于“普通用例”。@fishtoaster的规则2还将添加上面第3和第4行中的部分或全部。但是对于等效分区测试,每个等效类中的任何一个测试用例就足够了。如果选择“ a”或“ g”或“ w”,则您正在测试相同的代码路径。因此,您总共有4个测试用例,而不是52个以上。

边界值分析建议进行一些细化:实质上,这表明并非等效类的每个成员都相等。也就是说,边界值本身也应被认为值得进行测试。(对此,一个简单的理由就是臭名昭著的“一个接一个”错误!)因此,对于每个等效类,您可以有3个测试输入。查看上面的输入域-并具有一些ASCII值的知识-我可能会想到这些测试用例输入:

| # | Input                | # test cases |
| 1 | a, w, z              | 3            |
| 2 | A, E, Z              | 3            |
| 3 | 0, 5, 9, !, @, *, ~  | 7            |
| 4 | nul, esc, space, del | 4            |

(一旦获得三个以上的边界值,则表明您可能想重新考虑原始的等价类描述,但这非常简单,以至于我不打算修改它们。)因此,边界值分析使我们了解到17个测试用例(具有完全覆盖范围的高置信度)与128个测试用例进行详尽的测试相比。(更不用说组合运算法则表明穷举测试对于任何实际应用程序都是根本不可行的!)


3
+1这正是我凭直觉编写测试的方式。现在我可以在上面加上名称了:)感谢您的分享。
guillaume31 2013年

+1代表“定性答案很有用,但有可能-而且更可取-要定量”
Jimmy Breck-McKye 2015年

如果指令是“我如何才能很好地覆盖我的测试”,我认为这是一个很好的答案。我认为在此之上找到一种务实的方法将很有用-是否应该以这种方式彻底测试每一层逻辑的每个分支的目标?
基琳·约翰斯通

18

我的意见可能不太受欢迎。但是我建议您在进行单元测试时比较经济。如果单元测试太多,则很容易花费一半的时间或更多时间来维护测试,而不是实际编码。

我建议您为肠胃不适或非常关键和/或基本的事物编写测试。IMHO单元测试不能代替良好的工程和防御性编码。目前,我正在从事一个或多或少无法使用的项目。它确实很稳定,但是很难重构。实际上,一年内没有人碰过此代码,它所基于的软件堆栈已有4年的历史。为什么?准确地说,因为它充满了单元测试,所以:单元测试和自动集成测试。(是否听说过黄瓜之类的东西?)这是最好的部分:这个(尚未)无法使用的软件是由一家公司开发的,该公司的员工是测试驱动开发领域的先驱。:D

所以我的建议是:

  • 开发了基本框架之后就开始编写测试,否则重构会很痛苦。作为为他人开发的开发人员,您一开始就无法正确满足需求。

  • 确保可以快速执行单元测试。如果您有集成测试(例如黄瓜),则可以花费更长的时间。但是,相信我,长时间运行的测试并不有趣。(人们忘记了C ++变得不那么受欢迎的所有原因...)

  • 将此TDD内容留给TDD专家。

  • 是的,有时您会专注于边缘情况,有时会专注于常见情况,这取决于您期望发生意外的地方。尽管如果您始终希望遇到意外情况,则应该重新考虑工作流程和纪律。;-)


2
您能否详细说明为什么测试会使该软件难以重构?
麦克·帕特里奇

6
大+1。拥有测试实现而不是规则的单元测试墙会使任何更改需要2-3倍的数量
TheLQ 2011年

9
与编写不良的生产代码一样,编写不良的单元测试也很难维护。“太多的单元测试”听起来像是无法保持干燥。每个测试都应解决/证明系统的特定部分。
艾伦

1
每个单元测试都应该检查一件事,因此单元测试不会太多,但是缺少测试。如果您的单元测试很复杂,那就是另一个问题。
2014年

1
-1:我认为这篇文章写得不好。提到了很多事情,我不知道它们之间的关系。如果答案的重点是“经济”,那么您的例子到底有何关系?听起来您的示例情况(尽管真实)的单元测试很差。请解释一下我应该从中学到什么教训,以及它如何帮助我节省开支。另外,老实说,我只是不知道您说什么Leave this TDD stuff to the TDD-experts
亚历山大·伯德

8

如果您首先使用“测试驱动开发”进行测试,那么您的覆盖范围将达到90%或更高,因为如果不先为它编写失败的单元测试,就不会添加功能。

如果您要在事后添加测试,那么我不建议您获得迈克尔·费瑟斯Michael Feathers)的《有效使用旧版代码》的副本,并了解一些在代码中添加测试以及重构代码的方法使其更具可测试性。


您如何计算覆盖率?无论如何,覆盖90%的代码是什么意思?
zneak

2
@zneak:有代码覆盖率工具可以为您计算它们。一个快速的谷歌的“代码覆盖率”应提出其中的一些。该工具跟踪在运行测试时执行的代码行,并以此为基础来汇编程序集中的总代码行,以得出覆盖率。
史蒂文·埃弗斯

-1。没有回答问题:The problem is, I don't know _what_ to test
亚历山大·伯德

6

如果您开始遵循“ 测试驱动开发”实践,那么它们将指导您完成该过程,并知道要进行哪些测试。一些开始的地方:

测试优先

永远不要在编写测试之前编写代码。有关说明,请参见“ 红-绿-重构-重复 ”。

编写回归测试

每当遇到错误时,都编写一个测试用例,并确保它不会失败。除非您可以通过失败的测试用例重现错误,否则您还没有真正找到它。

红绿重构重复

红色:从编写最基本的测试开始,以测试您要实现的行为。将这一步想像为编写一些示例代码,这些示例代码使用您正在使用的类或函数。确保它编译/没有语法错误并且失败。这应该很明显:您尚未编写任何代码,所以它一定会失败,对吧?在这里要了解的重要一点是,除非您看到测试至少失败一次,否则您将无法确定测试是否通过,是由于某种虚假原因而您做了某些事情。

绿色:编写实际上使测试通过的最简单,最愚蠢的代码。不要试图变得聪明。即使您看到有明显的边缘情况,但测试已考虑在内,也不要编写代码来处理它(但不要忘记边缘情况:稍后再使用)。这个想法是,每一段代码编写哟,每一个if,每一个try: ... except: ...应该由一个测试用例合理的。该代码不必优雅,快速或优化。您只希望测试通过。

重构:清理代码,正确获取方法名称。查看测试是否仍然通过。优化。再次运行测试。

重复:您还记得测试没有涵盖的极端情况,对吗?因此,现在是重要时刻。编写一个涵盖这种情况的测试用例,观察它是否失败,编写一些代码,看看它是否通过,然后进行重构。

测试你的代码

您正在处理某些特定的代码,而这正是您要测试的内容。这意味着您不应测试库函数,标准库或编译器。另外,请尝试避免测试“世界”。这包括:调用外部Web API,一些数据库密集型内容等。只要您可以尝试对其进行模拟(使对象遵循相同的接口,但返回静态的预定义数据)。


1
假设我已经有一个现有的代码库(据我所知),我该怎么办?
zneak 2010年

这可能会稍微困难一些(取决于代码的编写方式)。从回归测试开始(它们总是很有意义),然后您可以尝试编写单元测试以向自己证明您了解代码在做什么。很容易被(看起来)要做的工作量淹没,但是:有些测试总比没有测试要好。
Ryszard Szopa 2010年

3
-1,我不认为这是一个很好的回答了这个问题。问题不关乎TDD,而是询问编写单元测试时要测试什么。我认为对实际问题的一个很好的答案应该适用于非TDD方法。
布莱恩·奥克利

1
如果触摸它,请对其进行测试。Clean Code(Robert C Martin)建议您为第三方代码编写“学习测试”。这样,您将学会使用它,并且可以进行测试,以防新版本更改您正在使用的行为。
罗杰·威尔考克斯

3

对于单元测试,首先要测试它是否达到了设计的目的。那应该是您写的第一种情况。如果设计的一部分是“如果传入垃圾,它应该抛出异常”,则也应进行测试,因为这是设计的一部分。

从那开始。当您获得进行最基本的测试的经验时,将开始学习这是否足够,并开始查看代码中需要测试的其他方面。


0

正确的答案是“测试所有可能破坏的东西”

有什么太简单而无法打破的?数据字段,死灵属性访问器和类似的样板开销。其他任何事情都可能实现需求的某些可识别部分,并且可能会从测试中受益。

当然,您的里程-和您的工作环境的做法-可能会有所不同。


好的。那么我应该测试哪些情况?“正常”情况?边缘情况?
zneak

3
经验法则?在黄金路径的中间,在任何边缘的内侧和外侧,分别有一两个。
杰弗里·汉汀

@JeffreyHantin这是另一个答案中的“边界值分析”。
罗杰·威尔考克斯
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.