基于属性的测试是测试的替代方法,是对基于示例的测试的补充。 后者是我们一生都在做的事情:针对“示例”行使生产代码-我们认为这些输入具有代表性。 挑选这些示例本身就是一门艺术:“普通”输入,边缘情况,格式错误的输入等。但是为什么我们将自己限制为仅几个示例呢? 为什么不测试成千上万个… 所有输入? 这种方法至少有两个困难:
- 规模。 一个仅接受一个
int
输入的纯函数将需要40亿次测试。 这意味着数百GB的测试源代码和几个月的执行时间。 如果一个函数需要两个int
,则将其平方。 对于String
来说,实际上达到了无穷大。 - 假设我们有在量子计算机或类似设备上执行的这些测试。 您如何知道每个特定输入的预期结果? 您可以手动输入(祝您好运),也可以生成预期的输出。 通过生成,我的意思是编写一个程序,为每个输入生成期望值。 但是我们不是首先就已经测试了这样的程序吗? 我们是否要编写更好的,无错误版本的被测代码只是为了对其进行测试? 也称为丑陋的镜子反图案 。
因此,您了解测试每一个输入(尽管比较理想)只是一项心理实验,无法实现。 话虽这么说,基于属性的测试试图尽可能地接近该测试的必杀技。 问题#1通过用数百或数千个随机输入猛击被测代码来解决。 不是全部,甚至不是一小部分。 但是是一个很好的随机表示。
问题2出奇地困难。 基于属性的测试可以生成随机参数,但是无法计算出该随机输入的预期结果。 因此,我们需要一种不同的机制来命名整个哲学。 我们必须提出无论输入是什么,被测代码都表现出的属性(不变性,行为)。 从理论上讲,这听起来很合理,但是在各种情况下都有许多这样的属性:
- 任何数字的 绝对值 都不应为负
- 编码和解码任何字符串都应为每种对称编码返回相同的
String
- 对于任何输入 ,某些旧算法的优化版本应产生与旧算法相同的结果
- 在以任意顺序进行任意数量的银行间交易之后,银行的总金额应保持不变
如您所见,我们可以想到许多属性,其中没有提到特定的示例输入。 这不是详尽而严格的测试。 这更像是采样,并确保样本“理智”。 有很多库支持几乎每种语言的基于属性的测试。 在本文中,我们稍后将探讨Spock和ScalaCheck。
Spock +自定义数据生成器
Spock不支持现成的基于属性的测试。 但是,在数据驱动测试和第三方数据生成器的帮助下,我们可以走得很远。 Spock中的数据表可以归纳为所谓的数据管道 :
def 'absolute value of #value should not be negative'() {
expect:
value.abs() >= 0
where:
value << randomInts(100)
}
private static def List<Integer> randomInts(int count) {
final Random random = new Random()
(1..count).collect { random.nextInt() }
}
上面的代码将生成100个随机整数,并确保所有.abs()
均为非负数。 您可能会认为该测试相当愚蠢,但是令人惊讶的是它实际上发现了一个错误! 但是首先让我们杀死一些样板代码。 产生随机输入,尤其是更复杂的输入,既麻烦又无聊。 我发现了两个可以帮助我们的库。 鸡胚的发生 :
import spock.genesis.Gen
def 'absolute value of #value should not be negative'() {
expect:
value.abs() >= 0
where:
value << Gen.int.take(100)
}
看起来不错,但是如果要生成例如随机整数列表, net.java.quickcheck
具有更好的API,并且不是特定于Groovy的:
import static net.java.quickcheck.generator.CombinedGeneratorsIterables.someLists
import static net.java.quickcheck.generator.PrimitiveGenerators.integers
def 'sum of non-negative numbers from #list should not be negative'() {
expect:
list.findAll{it >= 0}.sum() >= 0
where:
list << someLists(integers(), 100)
}
这个测试很有趣。 通过生成100个随机int
,确保非负数之和永远不会为负。 听起来很合理。 但是,多次测试均失败。 首先,由于整数溢出有时两个正int
■添加到一个负值。 ! 实际上,发现的另一种失败类型令人恐惧。 虽然[1,2,3].sum()
为6,但是[].sum()
为… null
( WAT? )
如您所见,即使是最简单,最基本的基于属性的测试也可以在数据中发现异常情况。 但是等等,我说测试int
绝对值发现了一个错误。 实际上,由于数据生成器差(“随机性”太差),它并不是没有首先返回已知的边沿值。 我们将在下一篇文章中解决该问题。
翻译自: https://www.javacodegeeks.com/2014/09/property-based-testing-with-spock.html