可以通过不变测试替换单元测试吗?

时间:2009-04-20 04:14:36

标签: unit-testing quickcheck

作为程序员,我全心全意地购买了TDD理念,并努力为我编写的任何重要代码进行广泛的单元测试。有时这条道路可能会很痛苦(行为变化会导致多个单元测试的级联变化;需要大量的脚手架),但总的来说,我拒绝编程而没有我可以在每次更改后运行的测试,而且我的代码就像结果

最近,我一直在玩Haskell,它是常驻测试库,QuickCheck。与TDD明显不同的是,QuickCheck强调测试代码的不变量,即保留输入的所有(或实质子集)的某些属性。一个简单的例子:一个稳定的排序算法应该给出相同的答案,如果我们运行它两次,应该有增加的输出,应该是输入的排列等。然后,QuickCheck生成各种随机数据,以测试这些不变量。

在我看来,至少对于纯函数(即没有副作用的函数 - 如果你正确地模拟你可以将脏函数转换成纯函数),那不变测试可以取代单元测试作为一个严格的超集这些能力。每个单元测试由输入和输出组成(在命令式编程语言中,“输出”不仅仅是函数的返回,而且还有任何已更改的状态,但这可以被封装)。可以想象,可以创建一个随机输入生成器,它足以覆盖您手动创建的所有单元测试输入(然后是一些,因为它会产生您根本不会想到的情况);如果由于某些边界条件而在程序中发现错误,则可以改进随机输入生成器,以便生成该情况。

然后,挑战在于是否有可能为每个问题制定有用的不变量。我要说的是:一旦你有一个答案,看它是否正确,首先计算答案就更简单了。对不变量的思考也有助于澄清复杂算法的规范,比特殊测试用例更好,这鼓励了对问题的逐个案例思考。您可以将程序的先前版本用作模型实现,或者使用另一种语言的程序版本。等等。最终,您可以覆盖所有以前的测试用例,而无需显式编码输入或输出。

我是疯了,还是我在做什么?

4 个答案:

答案 0 :(得分:21)

一年后,我现在认为我对这个问题有一个答案:不!特别是,单元测试对于回归测试总是必要且有用的,其中测试附加到bug报告并继续存在于代码库中,以防止该bug再次出现。

但是,我怀疑任何单元测试都可以替换为随机生成输入的测试。即使在命令式代码的情况下,“输入”也是您需要做出的命令式语句的顺序。当然,是否值得创建随机数据生成器,以及是否可以使随机数据生成器具有正确的分布是另一个问题。单元测试只是一种退化情况,随机生成器总是给出相同的结果。

答案 1 :(得分:9)

你提出的是一个非常好的观点 - 仅适用于函数式编程。你提出了一种用命令式代码完成这一切的方法,但是你也谈到了为什么它没有完成 - 这并不是特别容易。

我认为这就是它不能取代单元测试的原因:它不容易适用于命令式代码。

答案 2 :(得分:0)

你在原帖中写的内容,让我想起了这个问题,这是一个关于循环不变量来证明循环正确的一个悬而未决的问题......

无论如何,我不确定你在正式规范中读了多少,但是你正在沿着这条思路前进。大卫格里斯的书是关于这个主题的经典着作,我仍然没有很好地掌握这个概念,以便在我的日常编程中快速使用它。对正式规范的通常反应是,它既困难又复杂,只有你在安全关键系统上工作才值得努力。但我认为有一些信封技术类似于quickcheck可以使用的技术。

答案 3 :(得分:0)

我只听说过(未使用过)这类测试,但我看到了两个潜在的问题。我很乐意对每一个发表评论。

误导性结果

我听说过如下测试:

  • reverse(reverse(list))应该等于list
  • unzip(zip(data))应该等于data

很高兴知道这些适用于广泛的投入。 但如果函数只返回输入,这两个测试都会通过。

在我看来,您需要验证,例如,reverse([1 2 3])等于[3 2 1]以证明在至少一种情况下的正确行为,然后添加一些测试随机数据。

测试复杂性

完全描述输入和输出之间关系的不变测试可能比函数本身更复杂。如果它很复杂,它可能是错误的,但你没有测试你的测试。

相比之下,一个好的单元测试太简单了,不能像读者那样搞砸或误解。只有拼写错误才能在“期望reverse([1 2 3])等于[3 2 1]”中创建错误。