是否手动编写单元测试示例验证?


9

我们知道编写JUnit测试可以演示代码中的一条特定路径。

我的一位同事评论说:

手动编写单元测试是“ 通过示例证明”

他来自具有诸如Quickcheck之类的工具的Haskell的背景,并且具有推理类型程序行为的能力。

他的意思是,此方法未尝试使用许多其他输入组合,而您的代码并未经过测试。

我的问题是:手动编写单元测试示例验证吗?


3
不,不编写/使用测试。声称您的单元测试可以证明程序没有任何问题,这就是示例证明(不恰当的概括)。测试不是关于数学上证明代码正确性的测试-测试本质上是实验检查。这是一个安全网,可以通过告诉您一些有关代码的知识来帮助您建立信心。但是,您是必须选择一种好的策略来探究代码的人,并且您是必须解释数据含义的人。
FilipMilovanović17年

Answers:


10

如果您是随机选择要测试的输入,那么我想您可能正在行使“示例验证”逻辑谬误。

但是好的单元测试永远都做不到。相反,它们处理范围边缘情况。

例如,如果要为接受整数作为输入的绝对值函数编写单元测试,则无需测试输入的每个可能值即可证明代码有效。要进行全面测试,您只需要五个值:-1、0、1,以及输入整数的最大值和最小值。

这五个值测试功能的每个可能范围和边缘情况。您无需测试其他所有可能的输入值(即整数类型可以代表的每个数字),即可获得该函数适用于所有输入值的高置信度。


11
代码测试人员进入酒吧并点一杯啤酒。5杯啤酒。-1啤酒,MAX_VALUE啤酒,一只鸡。空值。
尼尔,

2
“ 5个值”纯粹是胡说八道。考虑一个像这样的琐碎函数int foo(int x) { return 1234/(x - 100); }。还要注意(取决于您要测试的内容),您可能需要确保无效(“超出范围”)输入返回正确的结果(例如,“ find_thing(thing)”正确返回某种“未找到”状态) (如果找不到)。
布伦丹

3
@布伦丹:五个值并没有什么意义。在我的示例中,它恰好是五个值。您的示例具有不同数量的测试,因为您要测试其他功能。我并不是说每个功能都需要五个测试。您是通过阅读我的答案推断出来的。
罗伯特·哈维

1
生成式测试库通常比您更擅长测试边缘案例。如果,例如,您使用浮点数而不是整数,你的图书馆还会检查-InfInfNaN1e-100-1e-100-02e200...我宁愿没有到所有手工做的那些。
Hovercouch

@Hovercouch:如果您知道一个好的,我很想听听。我见过的最好的是Pex;但是,它非常不稳定。记住,我们在这里谈论的是相对简单的功能。当您处理诸如现实的业务逻辑之类的事情时,事情会变得更加困难。
罗伯特·哈维

8

任何软件测试都类似于“示例验证”,而不仅仅是使用JUnit之类的工具进行单元测试。但这不是新的见识,Dijkstra引用了1960年的话,内容基本相同:

“测试表明存在缺陷,而不是没有缺陷”

(只需将“显示”一词替换为“证明”)。但是,对于生成随机测试数据的工具也是如此。实际功能的可能输入数量通常比生成的测试用例数量要大几个数量级,而不取决于生成这些用例的方法,并根据宇宙时期内的预期结果进行验证,因此即使使用生成器工具生成大量测试数据,也不能保证不会遗漏一个可能已经检测到某个错误的测试用例。

随机测试有时可能发现一个错误,而该错误被手动创建的测试用例忽略了。但是总的来说,对要测试的功能进行精心的测试,并确保使用尽可能少的测试用例获得完整的代码和分支覆盖范围,会更有效率。有时将手动和随机生成的测试结合起来是一种可行的策略。此外,在使用随机测试时,必须注意以可重复的方式获得结果。

因此,手动创建的测试绝不会比随机生成的测试差,通常恰恰相反。


1
任何使用随机检查的实用测试套件也将具有单元测试。(从技术上讲,单元测试只是随机测试的简并案例。)您的措辞表明,很难实现随机测试,或者很难将随机测试和单元测试结合起来。通常情况并非如此。在我看来,随机测试的最大好处之一是,它强烈鼓励编写测试,将其作为旨在始终保持的代码属性。我宁愿让这些属性明确声明(并检查!),也不必推断它们的一些点测试。
德里克·埃尔金斯

@DerekElkins:“难”是恕我直言的错误术语。随机测试需要付出一定的努力,而这种努力会减少手工测试的可用时间(并且,如果您的人只是遵循问题中提到的口号,那么他们可能根本不会进行手工制作)。只是在一段代码上抛出大量随机测试数据只是工作的一半,而且还必须为每个测试输入产生预期的结果。在某些情况下,这可以自动完成。对于其他人,不是。
布朗

虽然在某些时候肯定需要一些想法来选择一个好的发行版,但这通常不是一个大问题。您的评论暗示您正在以错误的方式思考。您为随机检查编写的属性与为模型检查或形式证明编写的属性相同。确实,它们可以并且已经同时用于所有这些东西。也没有需要产生的“预期结果”。相反,您只需声明一个应始终保持的属性。一些示例:1)将东西推入堆栈并...
德里克·埃尔金斯

...然后弹出应该等同于不执行任何操作;2)任何余额超过10,000美元的客户都应获得高余额利率,然后才能这样做;3)子画面的位置始终在屏幕的边界框内。一些属性很可能与点测试相对应,例如“当余额为$ 0时给出零余额警告”。这些属性是部分规格,理想情况是获得总体规格。难以考虑这些属性意味着您不清楚规格是什么,并且常常意味着您难以考虑好的单元测试。
德里克·埃尔金斯

0

手动编写测试是“举例说明”。但是QuickCheck也是如此,并在一定程度上键入系统。任何不经过形式化的正式验证的内容都将受到限制,它无法告诉您有关代码的信息。相反,您必须考虑方法的相对优点。

像QuickCheck这样的生成式测试对于扫除大量输入确实非常有用。处理边缘案例比手工测试要好得多:生成性测试库将比您有更多的经验。另一方面,它们仅告诉您不变量,而不是特定的输出。因此,要验证您的程序是否获得了正确的结果,您仍然需要进行一些手动测试以验证实际上foo(bar) = baz

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.