在 ScalaCheck 中,我有一个属性测试,它使用正整数生成器,当它失败时,ScalaCheck 将缩小到非正值。
收缩应该有助于找到最小的失败案例。收缩到所需范围之外的值会令人困惑且无济于事。这是一个已知的错误,请参阅ScalaCheck问题#129Gen.suchThat不尊重收缩
是否可以在作用域中定义我自己的收缩实例,以便它仅收缩到正整数?
下面是一个最简单的属性测试:
class ShrinkProp extends Properties("Shrink") {
property("posNum[Int]") = {
Prop.forAll(Gen.posNum[Int]) { _: Int =>
Prop.falsified
}
}
}
这通常会导致 ScalaCheck 将参数缩小到零:
[info] Done compiling. [info] ! Shrink.posNum[Int]: Falsified after 0 passed tests. [info] > ARG_0: 0 [info] > ARG_0_ORIGINAL: 1 [info] Failed: Total 1, Failed 1, Errors 0, Passed 0
或者更糟糕的是,它有时可能会缩小到负值:
[info] ! Shrink.posNum[Int]: Falsified after 5 passed tests. [info] > ARG_0: -1 [info] > ARG_0_ORIGINAL: 3 [info] Failed: Total 1, Failed 1, Errors 0, Passed 0
一种解决方案是使用< code>forAllNoShrink关闭收缩:
class ShrinkProp extends Properties("Shrink") {
property("posNum[Int]") = {
Prop.forAllNoShrink(Gen.posNum[Int]) { _: Int =>
Prop.falsified
}
}
}
结果是没有缩小:
[info] ! Shrink.posNum[Int]: Falsified after 0 passed tests. [info] > ARG_0: 1 [info] Failed: Total 1, Failed 1, Errors 0, Passed 0
另一种选择是在测试中添加一个保护,以便缩小值或跳过:
import Prop.BooleanOperators
class ShrinkProp extends Properties("Shrink") {
property("posNum[Int]") = {
Prop.forAll(Gen.posNum[Int]) { x: Int =>
(x >= 1) ==> Prop.falsified
}
}
}
除了禁用收缩和添加防护外,还有其他选择吗?
在 Scala 检查中没有正整数的收缩器。你必须写你自己的。
收缩
需要定义为属性测试范围内的隐式
内容。然后,Prop.forAll
将找到正确的收缩
类(如果它在作用域内,并且具有未通过测试的值的相应类型签名)。
从根本上讲,Shrink
实例是一个函数,它将失败的值x
trait Shrink[T] {
def shrink(x: T): Stream[T]
}
您可以使用伴随对象的 apply
方法定义 Shrink
,大致如下:
object Shrink {
def apply[T](s: T => Stream[T]): Shrink[T] = {
new Shrink[T] {
def shrink(x: T): Stream[T] = s(x)
}
}
}
正整数的收缩器是一种流
,它通过对值减半来收缩,以通过二进制搜索找到最小的失败情况,但在达到零之前停止:
class ShrinkProp extends Properties("Shrink") {
implicit val posIntShrinker: Shrink[Int] = Shrink { x: Int =>
Stream.iterate(x / 2) { x: Int =>
x / 2
}.takeWhile { x: Int =>
x > 0 // Avoid zero.
}
}
property("posNum[Int]") = {
Prop.forAll(Gen.posNum[Int]) { _: Int =>
Prop.falsified
}
}
}
证明故障正在起作用:
[info] ! Shrink.posNum[Int]: Falsified after 6 passed tests. [info] > ARG_0: 2 [info] > ARG_0_ORIGINAL: 4 [info] Failed: Total 1, Failed 1, Errors 0, Passed 0
更好的是,您可以编写一个属性来验证收缩器的行为是否正常:
property("posIntShrinker") = {
Prop.forAll { x: Int =>
val shrunk = Shrink.shrink(x)
Prop.atLeastOne(
(x >= 2) ==> shrunk.size > 0,
(x <= 1) ==> shrunk.isEmpty
)
}
}
[info] + Shrink.posIntShrinker: OK, passed 100 tests. [info] Failed: Total 1, Failed 0, Errors 0, Passed 1
最好编写一个通用正数Shrink
,它能够收缩其他类型的数字,例如Long
、浮点类型和BigDecimal
。