不变量测试能否替代单元测试?

36
作为一名程序员,我完全认同“测试驱动开发”的理念,并会为我写的任何非平凡代码编写广泛的单元测试。有时候这条路可能是痛苦的(行为变化导致级联多个单元测试的更改;需要大量的支架),但总的来说,我拒绝在没有可运行测试的情况下进行编程,结果我的代码出现漏洞的概率也少得多。
最近,我一直在尝试Haskell及其测试库QuickCheck。与TDD非常不同的是,QuickCheck强调测试代码的不变性,也就是某些属性在所有输入(或实质子集)上都成立。举个例子:一个稳定的排序算法如果我们运行两次应该会给出相同的答案并且输出递增,应该是输入的置换等。然后,QuickCheck会生成各种随机数据以测试这些不变量。
至少对于纯函数(即没有副作用的函数-如果你正确地进行模拟,可以将脏函数转换为纯函数),似乎不变性测试可以替代单元测试并严格是其超集。每个单元测试包括一个输入和一个输出(在命令式编程语言中,“输出”不仅仅是函数的返回值,还包括任何更改的状态,但这可以被封装)。可以想象创建一个随机输入生成器,它足以涵盖您手动创建的所有单元测试输入(并且还会生成您没有想到的案例);如果您发现程序中有错误,源自于某些边界条件,则可以改进随机输入生成器,使其也生成该案例。那么,挑战在于是否可以为每个问题制定有用的不变量。我认为是可能的:一旦你有一个答案,看看它是否正确比起一开始计算答案要简单得多。思考不变量也比临时测试用例更有助于澄清复杂算法的规范,后者鼓励了对问题逐个案例进行思考的方式。您可以使用程序的先前版本作为模型实现,或者使用另一种语言编写的程序版本等。最终,您可以涵盖所有以前的测试案例,而无需显式编码输入或输出。
我疯了吗,还是我发现了什么?

1
我最近一直在使用Erlang进行编程。虽然需要更多的工作来确定如何建模您的系统,但您是可以做到的。当您完成后,您会知道您的代码是好的。 - Zachary K
4个回答

24
一年后,我现在认为我有了这个问题的答案:不需要! 特别是针对回归测试,单元测试将始终是必要且有用的,在其中一个测试附加到缺陷报告中并存在于代码库中,以防止该缺陷再次出现。
但是,我怀疑任何单元测试都可以用其输入随机生成的测试替换。即使在命令式代码的情况下,“输入”也是您需要进行的命令式语句顺序。当然,是否值得创建随机数据生成器以及是否可以使随机数据生成器具有正确的分布是另一个问题。单元测试只是一种退化情况,其中随机生成器始终给出相同的结果。

你说得对,它不能替代更通用的单元测试,但是它绝对可以补充整体的测试工作。 - tomosius
2
你不能只是确保使用相同的随机种子吗?或者以某种方式保存生成的随机输入?这个答案似乎基于属性测试框架可能缺乏功能,而不是基于属性测试本身的能力。 - lastmjs

9

你提出的观点非常好 - 只适用于函数式编程。你提到了一种使用命令式代码完成所有操作的方法,但也提到了为什么不这样做 - 这并不特别容易。

我认为这正是它无法取代单元测试的原因:它不太适合命令式代码。


这是一个很好的观点!接下来的问题是,我们能否将其适应命令式代码? - Edward Z. Yang
2
它肯定可以 - 但这与首先选择语言范式的论点相同;它们都是“平等”的,但有些地方某些事情就是不适合。我认为这是其中之一。 - Anthony

1

有疑虑

我只听说过(没有使用过)这些测试,但我看到了两个潜在的问题。我希望每个问题都能得到评论。

结果误导

我听说过像这样的测试:

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

如果函数只返回其输入,则这两个测试都将通过。很高兴知道这些测试对各种输入都成立。但是,如果你想证明至少一个情况下正确的行为,那么你需要验证例如reverse([1 2 3])等于[3 2 1],然后添加一些随机数据进行测试。

测试复杂度

描述输入和输出之间关系的不变测试可能比函数本身更复杂。如果它很复杂,它可能会有缺陷,但你没有测试来测试你的测试。

相比之下,一份良好的单元测试应该是简单到读者不会出现错误或误解的程度。只有打错字才可能导致 "期望 reverse([1 2 3]) 等于 [3 2 1]" 出现 bug。

2
就我个人而言,我不会写“reverse = id”,所以我相当有信心,如果我真的尝试正确地实现它,并且通过了“reverse . reverse = id”测试,那么它很可能是正确的。 - alternative
2
你真正想要的不变量是 forall x xs i, (x:xs) !! j == reverse (x:xs) !! (n - j),其中 n = length xsj = mod i (n + 1) - isekaijin
1
我可以保证任何满足上述不变量和平凡的 reverse[]=[] 的东西都是真正的反转函数。不需要单元测试。 - isekaijin

0

你在原帖中写的内容让我想起了这个问题,即关于循环不变量如何证明循环正确性的一个开放性问题...

无论如何,我不确定你在形式规范方面读了多少,但你正在朝着那条思路前进。David Gries的书是该主题的经典之一,我仍然没有掌握足够好的概念来在日常编程中迅速使用它。对于形式规范的通常反应是,它很难、很复杂,只有在工作于安全关键系统时才值得付出努力。但我认为有类似于QuickCheck所暴露的信封背面技术可以使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接