提问者:小点点

正数上的属性不应该缩小到负数


在 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
    }
  }
}

除了禁用收缩和添加防护外,还有其他选择吗?


共1个答案

匿名用户

在 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