在 ScalaCheck 中,我编写了一个非空字符串列表的生成器,
val nonEmptyListsOfString: Gen[List[String]] =
Gen.nonEmptyListOf(Arbitrary.arbitrary[String])
然后,假设我使用 Prop.forAll
编写了一个属性,
Prop.forAll(nonEmptyListsOfString) { strs: List[String] =>
strs == Nil
}
这只是一个简单的示例,旨在失败,因此它可以显示 Scalacheck 如何完成收缩以找到最小的示例。
但是,Scalacheck 中的默认收缩器不尊重生成器,并且仍将收缩为空字符串,忽略生成器属性。
sbt> test
[info] ! Prop.isEmpty: Falsified after 1 passed tests.
[info] > ARG_0: List()
[info] > ARG_0_ORIGINAL: List("")
[info] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error] example.Prop
如评论中所述,并重用您发布的 github 问题中的示例:
import cats.data.NonEmptyList
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.{FreeSpec, Matchers}
import org.scalatest.prop.PropertyChecks
class ScalaCheckTest extends FreeSpec with PropertyChecks with Matchers{
"Test scalacheck (failing)" in {
val gen: Gen[List[Int]] = for {
n <- Gen.choose(1, 3)
list <- Gen.listOfN(n, Gen.choose(0, 9))
} yield list
forAll(gen) { list =>
list.nonEmpty shouldBe true
if (list.sum < 18) throw new IllegalArgumentException("ups")
}
}
"Test scalacheck" in {
val gen1 = for{
first <- Arbitrary.arbInt.arbitrary
rest <- Gen.nonEmptyListOf(Arbitrary.arbInt.arbitrary)
} yield {
NonEmptyList(first, rest)
}
forAll(gen1) { list =>
val normalList = list.toList
normalList.nonEmpty shouldBe true
if (normalList.sum < 18) throw new IllegalArgumentException("ups")
}
}
}
第一个测试确实失败,显示正在使用的空列表,但第二个测试确实会引发异常。更新:猫显然不是真正需要的,为了这个测试,我在这里使用了一个简单(和本地(版本的非空列表。
"Test scalacheck 2" in {
case class FakeNonEmptyList[A](first : A, tail : List[A]){
def toList : List[A] = first :: tail
}
val gen1 = for{
first <- Arbitrary.arbInt.arbitrary
rest <- Gen.nonEmptyListOf(Arbitrary.arbInt.arbitrary)
} yield {
FakeNonEmptyList(first, rest)
}
forAll(gen1) { list =>
val normalList = list.toList
normalList.nonEmpty shouldBe true
if (normalList.sum < 18) throw new IllegalArgumentException("ups")
}
}
有一种方法可以在 ScalaCheck 中定义自己的Shrink
类。 但是,这并不常见,也不容易做到。
概述
Shrink
需要在属性测试的范围内定义implicit
定义。 然后,如果Shrink
类在范围内,并且具有测试失败的值的适当类型签名,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)
}
}
}
示例:缩小整数
如果你知道如何在 Scala 中使用 Stream
集合,那么很容易为Int
定义一个收缩器,通过将值减半来收缩:
implicit val intShrinker: Shrink[Int] = Shrink {
case 0 => Stream.empty
case x => Stream.iterate(x / 2)(_ / 2).takeWhile(_ != 0) :+ 0
}
我们希望避免将原始值返回给 ScalaCheck,因此这就是为什么零是一个特例。
答:非空列表
对于非空字符串列表,您希望重用 ScalaCheck 的容器收缩,但避免使用空容器。 不幸的是,这并不容易做到,但有可能:
implicit def shrinkListString(implicit s: Shrink[String]): Shrink[List[String]] = Shrink {
case Nil => Stream.empty[List[String]]
case strs => Shrink.shrink(strs)(Shrink.shrinkContainer).filter(!_.isEmpty)
}
上面的一个不是编写避免空容器的通用容器收缩器,而是特定于List[String]
。 它可能可以重写为List[T]
.与Nil
的第一场模式匹配可能是不必要的。