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

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

我总是感到很难受,尽管测试将证明该函数在特定情况下有效,但对证明该函数有效完全没有用,期间。

评论

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

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

#1 楼

到目前为止,我的个人理念是:


测试所有可能的常见情况。这将告诉您进行某些更改后代码何时中断(我认为这是自动化单元测试的最大好处)。
测试一些您认为可能会异常复杂的代码的极端情况出现错误。
只要发现错误,就在修复它之前编写一个测试用例以掩盖它。
每当有人有时间杀人时,向不太重要的代码中添加边缘案例测试。


评论


多亏了这一点,我在这里一直困扰着OP。

–斯蒂芬
10 Nov 27'2:13

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

– mikera
2012年6月30日10:49

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

–ryno
15年10月20日在1:04

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

–ankush981
16年6月7日在9:28

您的答案在此热门媒体文章中得到了体现:hackernoon.com/…

–BugHunterUK
17-10-7在20:46

#2 楼

迄今为止,在众多答案中,没有人涉及等效划分和边界值分析,这是眼前问题的重要考虑因素。
所有其他答案虽然很有用,但都具有质性,但有可能-最好-在这里要定量。 @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            |


(您一旦获得超过3个边界值建议您可能需要重新考虑您的原始等价类描述,但这非常简单,以至于我没有再去修改它们。)因此,边界值分析使我们仅涉及17个测试用例-完全覆盖的信心很高-与进行详尽测试的128个测试用例相比。 (更不用说组合运算法则表明穷举测试对于任何实际应用都是根本不可行的!)

评论


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

– guillaume31
2013年11月22日14:59



+1对于“定性答案很有用,但有可能-并且更可取-是定量的”

–吉米·布雷克·麦基(Jimmy Breck-McKye)
2015年6月4日14:05

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

–基恩·约翰斯通(Kieren Johnstone)
16 Dec 6'在11:23

#3 楼

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

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

所以我的建议是:开发基本骨架后开始编写测试,否则重构会很痛苦。作为为他人开发的开发人员,您永远不会一开始就满足需求。
请确保可以快速执行单元测试。如果您有集成测试(例如黄瓜),可以花一些时间来确定。但是,请相信我,长时间运行的测试并不有趣。 (人们会忘记C ++变得不那么受欢迎的所有原因...)
将这些TDD内容留给TDD专家。
是的,有时您专注于边缘情况,有时则关注常见情况,取决于您对意外情况的预期。尽管如果您始终希望遇到意外情况,则应重新考虑工作流程和纪律。 ;-)


评论


您能否详细说明为什么测试会使该软件难以重构?

–迈克·帕特里奇(Mike Partridge)
2011年8月21日在18:17

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

–TheLQ
2011年8月22日0:00



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

–艾伦
13年11月22日在6:20

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

–graffic
2014年4月6日13:41

-1:我认为这篇文章写得不好。提到了很多事情,我不知道它们之间的关系。如果答案是“经济的”,那么您的例子到底有何关系?听起来您的示例情况(尽管真实)的单元测试很差。请解释一下我应该从中学到什么教训,以及它如何帮助我节省开支。另外,老实说,当您说将TDD内容留给TDD专家时,根本不知道您的意思。

–亚历山大·伯德(Alexander Bird)
18年7月18日在19:16



#4 楼

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

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

评论


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

–zneak
2010-09-3 19:29

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

–史蒂文·埃弗斯(Steven Evers)
2010-09-24 22:04

-1。不回答问题:问题是,我不知道要测试的_what_

–亚历山大·伯德(Alexander Bird)
18年7月18日在19:20

#5 楼

如果您开始遵循“测试驱动开发”实践,则它们将指导您完成该过程,并知道要进行哪些测试。一些开始的地方:
测试首先出现
永远不要在编写测试之前编写代码。请参阅Red-Green-Refactor-Repeat以获得解释。
编写回归测试
每当遇到错误时,请编写一个测试用例,并确保其失败。除非可以通过失败的测试用例重现错误,否则您还没有真正找到它。
红色-绿色-重构-重复
红色:首先,针对要尝试的行为编写最基本的测试实施。将这一步想像为编写一些示例代码,这些示例代码使用您正在使用的类或函数。确保它编译/没有语法错误并且失败。这应该很明显:您尚未编写任何代码,所以它一定会失败,对吧?在这里要了解的重要一点是,除非您看到测试至少失败一次,否则您将无法确定测试是否通过,是由于某种虚假原因而您做了某些事情。
绿色:编写最简单,最愚蠢的代码,使测试真正通过。不要试图变得聪明。即使您看到有明显的边缘情况,但测试已考虑在内,也不要编写代码来处理它(但请不要忘记边缘情况:稍后将需要它)。这个想法是,每个代码段,每个if,每个try: ... except: ...都应由一个测试用例来证明。该代码不必优雅,快速或优化。您只希望测试通过。
重构:清理代码,获取正确的方法名称。查看测试是否仍然通过。优化。再次运行测试。
重复:您还记得测试没有涵盖的边缘情况,对吗?所以,现在是重要时刻。编写一个涵盖这种情况的测试用例,观察它是否失败,编写一些代码,查看它是否通过,然后进行重构。
测试您的代码
您正在处理某些特定的代码,而这正是您要测试的内容。这意味着您不应测试库函数,标准库或编译器。另外,请尝试避免测试“世界”。这包括:调用外部Web API,一些数据库密集型内容等。只要您可以尝试对其进行模拟(使对象遵循相同的接口,但返回静态的预定义数据)。

评论


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

–zneak
10-10-13在1:25



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

–Ryszard Szopa
10-10-13在8:01

-1我认为这不是一个很好的答案。问题不在于TDD,而是在编写单元测试时询问要测试什么。我认为对实际问题的一个很好的答案应该适用于非TDD方法。

–布莱恩·奥克利(Bryan Oakley)
2011年8月21日在22:22

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

– Roger Willcocks
19-2-25在23:38

#6 楼

对于单元测试,首先要测试它是否可以完成其预期的工作。那应该是您写的第一种情况。如果设计的一部分是“如果传入垃圾将引发异常”,也应进行测试,因为这是设计的一部分。

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

#7 楼

基本的答案是“测试所有可能破坏的东西”。

有什么太容易破坏的东西?数据字段,死灵属性访问器和类似的样板开销。

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

评论


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

–zneak
2010-09-25 18:35

经验法则?在黄金路径的中间,在任何边缘的内侧和外侧,分别是一两个。

– Jeffrey Hantin
2010-09-27 21:45

@JeffreyHantin这是另一个答案中的“边界值分析”。

– Roger Willcocks
19-2-25在23:40