我刚大学毕业,下周要去上大学。我们已经看到了单元测试,但是我们并没有太多地使用它们。每个人都在谈论他们,所以我想也许我应该做些。
问题是,我不知道要测试什么。我应该测试一下普通情况吗?边缘情况?我怎么知道一个功能已被充分覆盖?
我总是有一种可怕的感觉,尽管测试可以证明某个功能在特定情况下可以工作,但是证明该功能在一定时期内是完全没有用的。
我刚大学毕业,下周要去上大学。我们已经看到了单元测试,但是我们并没有太多地使用它们。每个人都在谈论他们,所以我想也许我应该做些。
问题是,我不知道要测试什么。我应该测试一下普通情况吗?边缘情况?我怎么知道一个功能已被充分覆盖?
我总是有一种可怕的感觉,尽管测试可以证明某个功能在特定情况下可以工作,但是证明该功能在一定时期内是完全没有用的。
Answers:
到目前为止,我的个人哲学是:
迄今为止,在众多答案中,没有人涉及等效划分和边界值分析,这是眼前问题解答中的重要考虑因素。所有其他答案虽然很有用,但都是定性的,但有可能(最好是)在这里定量。@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个测试用例进行详尽的测试相比。(更不用说组合运算法则表明穷举测试对于任何实际应用程序都是根本不可行的!)
我的意见可能不太受欢迎。但是我建议您在进行单元测试时比较经济。如果单元测试太多,则很容易花费一半的时间或更多时间来维护测试,而不是实际编码。
我建议您为肠胃不适或非常关键和/或基本的事物编写测试。IMHO单元测试不能代替良好的工程和防御性编码。目前,我正在从事一个或多或少无法使用的项目。它确实很稳定,但是很难重构。实际上,一年内没有人碰过此代码,它所基于的软件堆栈已有4年的历史。为什么?准确地说,因为它充满了单元测试,所以:单元测试和自动集成测试。(是否听说过黄瓜之类的东西?)这是最好的部分:这个(尚未)无法使用的软件是由一家公司开发的,该公司的员工是测试驱动开发领域的先驱。:D
所以我的建议是:
在开发了基本框架之后就开始编写测试,否则重构会很痛苦。作为为他人开发的开发人员,您一开始就无法正确满足需求。
确保可以快速执行单元测试。如果您有集成测试(例如黄瓜),则可以花费更长的时间。但是,相信我,长时间运行的测试并不有趣。(人们忘记了C ++变得不那么受欢迎的所有原因...)
将此TDD内容留给TDD专家。
是的,有时您会专注于边缘情况,有时会专注于常见情况,这取决于您期望发生意外的地方。尽管如果您始终希望遇到意外情况,则应该重新考虑工作流程和纪律。;-)
Leave this TDD stuff to the TDD-experts
。
如果您首先使用“测试驱动开发”进行测试,那么您的覆盖范围将达到90%或更高,因为如果不先为它编写失败的单元测试,就不会添加功能。
如果您要在事后添加测试,那么我不建议您获得迈克尔·费瑟斯(Michael Feathers)的《有效使用旧版代码》的副本,并了解一些在代码中添加测试以及重构代码的方法使其更具可测试性。
The problem is, I don't know _what_ to test
如果您开始遵循“ 测试驱动开发”实践,那么它们将指导您完成该过程,并知道要进行哪些测试。一些开始的地方:
测试优先
永远不要在编写测试之前编写代码。有关说明,请参见“ 红-绿-重构-重复 ”。
编写回归测试
每当遇到错误时,都编写一个测试用例,并确保它不会失败。除非您可以通过失败的测试用例重现错误,否则您还没有真正找到它。
红绿重构重复
红色:从编写最基本的测试开始,以测试您要实现的行为。将这一步想像为编写一些示例代码,这些示例代码使用您正在使用的类或函数。确保它编译/没有语法错误并且失败。这应该很明显:您尚未编写任何代码,所以它一定会失败,对吧?在这里要了解的重要一点是,除非您看到测试至少失败一次,否则您将无法确定测试是否通过,是由于某种虚假原因而您做了某些事情。
绿色:编写实际上使测试通过的最简单,最愚蠢的代码。不要试图变得聪明。即使您看到有明显的边缘情况,但测试已考虑在内,也不要编写代码来处理它(但不要忘记边缘情况:稍后再使用)。这个想法是,每一段代码编写哟,每一个if
,每一个try: ... except: ...
应该由一个测试用例合理的。该代码不必优雅,快速或优化。您只希望测试通过。
重构:清理代码,正确获取方法名称。查看测试是否仍然通过。优化。再次运行测试。
重复:您还记得测试没有涵盖的极端情况,对吗?因此,现在是重要时刻。编写一个涵盖这种情况的测试用例,观察它是否失败,编写一些代码,看看它是否通过,然后进行重构。
测试你的代码
您正在处理某些特定的代码,而这正是您要测试的内容。这意味着您不应测试库函数,标准库或编译器。另外,请尝试避免测试“世界”。这包括:调用外部Web API,一些数据库密集型内容等。只要您可以尝试对其进行模拟(使对象遵循相同的接口,但返回静态的预定义数据)。
对于单元测试,首先要测试它是否达到了设计的目的。那应该是您写的第一种情况。如果设计的一部分是“如果传入垃圾,它应该抛出异常”,则也应进行测试,因为这是设计的一部分。
从那开始。当您获得进行最基本的测试的经验时,将开始学习这是否足够,并开始查看代码中需要测试的其他方面。