There is a way to define your own Shrink
class in ScalaCheck. However, it is not common nor very easy to do.
Overview
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)
}
}
}
Example: Shrinking integers
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.
Answer: Non-empty lists
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.