如何缩小列表但保证它不为空?



在 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的第一场模式匹配可能是不必要的。

最新更新