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

时间:2018-04-30 15:45:45

标签: scala scalacheck

ScalaCheck中,我编写了一个非空字符串列表生成器,

val nonEmptyListsOfString: Gen[List[String]] =
  Gen.nonEmptyListOf(Arbitrary.arbitrary[String])

然后,假设我使用Prop.forAll

写了一个属性
Prop.forAll(nonEmptyListsOfString) { strs: List[String] =>
  strs == Nil
}

这只是一个失败的简单示例,因此它可以显示Scalacheck如何通过缩小来找到最小的示例。

但是,Scalacheck中的默认收缩器不会respect the generator,并且仍会缩小为空字符串,忽略生成器属性。

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

2 个答案:

答案 0 :(得分:0)

如评论中所述,并重新使用您发布的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")
    }
  }

答案 1 :(得分:0)

有一种方法可以在ScalaCheck中定义自己的Shrink类。但是,这并不常见也不容易。

概述

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

示例:缩小整数

如果您知道如何使用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的第一个模式匹配。