具有正数的属性不应缩小为负数

时间:2018-11-26 14:51:52

标签: scala scalacheck

在ScalaCheck中,我进行了一个属性测试,该测试使用正整数生成器,当它失败时,ScalaCheck将缩小为非正值。

收缩应该有助于找到最小的失败案例。缩小到所需范围之外的值会造成混乱和无益。这是一个已知的错误,请参见ScalaCheck issue #129 Gen.suchThat not respected by shrinking

是否可以在范围内定义自己的收缩实例,使其仅收缩为正整数?

示例

这是一个最小的属性测试:

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

禁用收缩

一种解决方案是用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 个答案:

答案 0 :(得分:0)

ScalaCheck中的正整数没有收缩。你必须自己写。

概述

在您的属性测试范围内,需要将Shrink定义为implicit。然后Prop.forAll将找到正确的Shrink类,如果它在范围内,并且具有通过测试失败的值的适当类型签名。

从根本上讲,Shrink实例是将失败值x转换为“收缩”值流的函数。它的类型签名大致为:

trait Shrink[T] {
  def shrink(x: T): Stream[T]
}

您可以使用伴随对象的Shrink方法定义apply,大致是这样:

object Shrink {
  def apply[T](s: T => Stream[T]): Shrink[T] = {
    new Shrink[T] {
      def shrink(x: T): Stream[T] = s(x)
    }
  }
}

答案:缩小正整数

正整数的收缩器是Stream,它通过将值减半以通过二进制搜索找到最小的失败情况而收缩,但是在达到零之前停止:

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,这是很好的。