【问题标题】:A property over positive numbers shouldn't shrink to negative numbers超过正数的属性不应缩小为负数
【发布时间】:2018-11-27 00:25:48
【问题描述】:

在 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 将参数缩小到零:

[信息] 完成编译。 [信息]! Shrink.posNum[Int]:0 次通过测试后伪造。 [信息] > ARG_0: 0 [信息] > ARG_0_ORIGINAL:1 [信息] 失败:总计 1,失败 1,错误 0,通过 0

或者更糟的是,它有时会缩小到负值:

[信息]! Shrink.posNum[Int]:5 次通过测试后伪造。 [信息] > ARG_0:-1 [信息] > ARG_0_ORIGINAL:3 [信息] 失败:总计 1,失败 1,错误 0,通过 0

禁用收缩

一种解决方案是使用forAllNoShrink 关闭收缩:

class ShrinkProp extends Properties("Shrink") {
  property("posNum[Int]") = {
    Prop.forAllNoShrink(Gen.posNum[Int]) { _: Int =>
      Prop.falsified
    }
  }
}

结果没有缩水:

[信息]! Shrink.posNum[Int]:0 次通过测试后伪造。 [信息] > ARG_0: 1 [信息] 失败:总计 1,失败 1,错误 0,通过 0

添加警卫

另一种选择是在测试中添加一个守卫,以便他缩小值或只是跳过:

import Prop.BooleanOperators

class ShrinkProp extends Properties("Shrink") {
  property("posNum[Int]") = {
    Prop.forAll(Gen.posNum[Int]) { x: Int =>
      (x >=  1) ==> Prop.falsified
    }
  }
}

除了禁用收缩和添加保护之外,还有其他方法吗?

【问题讨论】:

    标签: scala scalacheck


    【解决方案1】:

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

    概述

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

    从根本上说,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)
        }
      }
    }
    

    答案:缩小正整数

    正整数的收缩器是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
        }
      }
    }
    

    证明失败是有效的:

    [信息]! Shrink.posNum[Int]:在 6 次通过测试后伪造。 [信息] > ARG_0: 2 [信息] > ARG_0_ORIGINAL:4 [信息] 失败:总计 1,失败 1,错误 0,通过 0

    更好的是,您可以编写一个属性来验证您的收缩器是否正常运行:

    property("posIntShrinker") = {
      Prop.forAll { x: Int =>
        val shrunk = Shrink.shrink(x)
        Prop.atLeastOne(
          (x >= 2) ==> shrunk.size > 0,
          (x <= 1) ==> shrunk.isEmpty
        )
      }
    }
    
    [信息] + Shrink.posIntShrinker:好的,通过了 100 次测试。 [信息] 失败:总计 1,失败 0,错误 0,通过 1

    编写一个通用的正数Shrink 会很好,它可以缩小其他类型的数字,例如Long、浮点类型和BigDecimal

    【讨论】:

      猜你喜欢
      • 2020-12-16
      相关资源
      最近更新 更多