34

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

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

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

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

我是不是疯了,还是我在做某事?

4

4 回答 4

24

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

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

于 2010-09-06T02:39:02.883 回答
9

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

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

于 2009-04-20T04:24:59.613 回答
1

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

误导性结果

我听说过这样的测试:

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

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

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

测试复杂性

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

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

于 2013-11-19T13:48:34.433 回答
0

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

无论如何,我不确定您在正式规范中阅读了多少,但您正在沿着这条思路前进。david gries 的书是这方面的经典著作之一,但我还没有很好地掌握这个概念,无法在我的日常编程中快速使用它。对正式规范的通常反应是,它既困难又复杂,只有在您从事安全关键系统工作时才值得付出努力。但我认为有类似于可以使用的快速检查暴露的信封技术。

于 2012-03-20T01:03:15.653 回答