Reputation: 4283
In ScalaCheck, I have written a generator of non-empty lists of strings,
val nonEmptyListsOfString: Gen[List[String]] =
Gen.nonEmptyListOf(Arbitrary.arbitrary[String])
And then, assume I wrote a property using Prop.forAll
,
Prop.forAll(nonEmptyListsOfString) { strs: List[String] =>
strs == Nil
}
This is just a simple example that is meant to fail, so that it can show how the shrinking is done by Scalacheck to find the smallest example.
However, the default shrinker in Scalacheck doesn't respect the generator, and will still shrink to an empty string, ignoring the generator properties.
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
Upvotes: 3
Views: 1012
Reputation: 4283
There is a way to define your own Shrink
class in ScalaCheck. However, it is not common nor very easy to do.
A Shrink
requires defining an implicit
definition in scope of your property test. Then Prop.forAll
will find your Shrink
class if it is in scope and has the appropriate type signature for the value that failed a test.
Fundamentally, a Shrink
instance is a function that converts the failing value, x
, to a stream of "shrunken" values. It's type signature is roughly:
trait Shrink[T] {
def shrink(x: T): Stream[T]
}
You can define a Shrink
with the companion object's apply
method, which is roughly this:
object Shrink {
def apply[T](s: T => Stream[T]): Shrink[T] = {
new Shrink[T] {
def shrink(x: T): Stream[T] = s(x)
}
}
}
If you know how to work with a Stream
collection in Scala, then it's easy to define a shrinker for Int
that shrinks by halving the value:
implicit val intShrinker: Shrink[Int] = Shrink {
case 0 => Stream.empty
case x => Stream.iterate(x / 2)(_ / 2).takeWhile(_ != 0) :+ 0
}
We want to avoid returning the original value to ScalaCheck, so that's why zero is a special case.
In the case of a non-empty list of strings, you want to re-use the container shrinking of ScalaCheck, but avoid empty containers. Unfortunately, that's not easy to do, but it is possible:
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)
}
Rather than writing a generic container shrinker that avoids empty containers, the one above is specific to List[String]
. It could probably be rewritten to List[T]
.
The first pattern match against Nil
is probably unnecessary.
Upvotes: 1
Reputation: 3988
As mentioned in the comment, and re-using the example from the github issue you posted:
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")
}
}
}
The first test does fail showing an empty list being used, but the second one does indeed throw the exception.
UPDATE: Cats is obviously not really needed, here I use a simple (and local) version of a non-empty list for the sake of this test.
"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")
}
}
Upvotes: 1