A comprehensive property-based testing library for Scala and Java applications that enables developers to specify program properties as testable assertions and automatically generates test cases to verify these properties.
—
ScalaCheck's shrinking framework automatically minimizes failing test cases to find the smallest counterexample. When a property fails, shrinking attempts to reduce the failing input to its essential elements, making debugging more effective by removing irrelevant complexity.
The fundamental shrinking abstraction that generates progressively smaller versions of failing inputs.
sealed abstract class Shrink[T] {
def shrink(x: T): Stream[T]
def suchThat(f: T => Boolean): Shrink[T]
}
object Shrink {
def apply[T](s: T => Stream[T]): Shrink[T]
def shrink[T](x: T)(implicit s: Shrink[T]): Stream[T]
def shrinkWithOrig[T](x: T)(implicit s: Shrink[T]): Stream[T]
}Usage Examples:
// Custom shrinking strategy
implicit val shrinkEvenInt: Shrink[Int] = Shrink { n =>
if (n % 2 == 0 && n != 0) {
Stream(n / 2, 0) ++ Stream.from(1).take(math.abs(n) - 1).filter(_ % 2 == 0)
} else Stream.empty
}
// Filter shrunk values
val positiveIntShrink = Shrink.shrinkIntegral[Int].suchThat(_ > 0)
// Apply shrinking manually
val shrunkValues = Shrink.shrink(100) // Stream(0, 50, 75, 88, 94, 97, 99, ...)The default shrinking strategy that provides no shrinking for unknown types.
implicit def shrinkAny[T]: Shrink[T] // No shrinking by defaultUsage Examples:
case class CustomType(value: String)
// By default, CustomType won't shrink
val noShrinkProp = forAll { (ct: CustomType) =>
ct.value.length >= 0 // If this fails, no shrinking occurs
}
// To enable shrinking, provide custom instance
implicit val shrinkCustomType: Shrink[CustomType] = Shrink { ct =>
Shrink.shrink(ct.value).map(CustomType(_))
}Automatic shrinking for all numeric types using mathematical reduction strategies.
implicit def shrinkIntegral[T](implicit num: Integral[T]): Shrink[T]
implicit def shrinkFractional[T](implicit num: Fractional[T]): Shrink[T]Usage Examples:
val intProp = forAll { (n: Int) =>
n != 42 // If this fails with n=42, shrinking tries: 0, 21, 32, 37, 40, 41
}
val doubleProp = forAll { (d: Double) =>
d < 100.0 // If this fails with d=150.5, shrinking tries progressively smaller values
}
val bigIntProp = forAll { (bi: BigInt) =>
bi < BigInt(1000) // Shrinking works for arbitrary precision integers
}Specialized string shrinking that reduces both length and character complexity.
implicit val shrinkString: Shrink[String]Usage Examples:
val stringProp = forAll { (s: String) =>
!s.contains("bug") // If fails with "debugger", shrinks to "bug"
}
// String shrinking strategies:
// 1. Remove characters from ends and middle
// 2. Replace complex characters with simpler ones
// 3. Try empty string
// Example: "Hello123!" -> "Hello123", "Hello", "Hell", "H", ""Automatic shrinking for all collection types, reducing both size and element complexity.
implicit def shrinkContainer[C[_], T](
implicit s: Shrink[T],
b: Buildable[T, C[T]]
): Shrink[C[T]]
implicit def shrinkContainer2[C[_, _], T, U](
implicit st: Shrink[T],
su: Shrink[U],
b: Buildable[(T, U), C[T, U]]
): Shrink[C[T, U]]Usage Examples:
val listProp = forAll { (l: List[Int]) =>
l.sum != 100 // If fails with List(25, 25, 25, 25), shrinks to List(100), then List(50, 50), etc.
}
val mapProp = forAll { (m: Map[String, Int]) =>
m.size < 5 // Shrinks by removing entries and shrinking remaining keys/values
}
val setProp = forAll { (s: Set[Double]) =>
!s.exists(_ > 1000.0) // Shrinks set size and individual elements
}
// Vector, Array, Seq, and other collections automatically get shrinking
val vectorProp = forAll { (v: Vector[String]) =>
v.forall(_.length < 10) // Shrinks vector size and individual strings
}Shrinking strategies for Option, Either, Try, and other wrapper types.
implicit def shrinkOption[T](implicit s: Shrink[T]): Shrink[Option[T]]
implicit def shrinkEither[T1, T2](
implicit s1: Shrink[T1],
s2: Shrink[T2]
): Shrink[Either[T1, T2]]
implicit def shrinkTry[T](implicit s: Shrink[T]): Shrink[Try[T]]Usage Examples:
val optionProp = forAll { (opt: Option[List[Int]]) =>
opt.map(_.sum).getOrElse(0) < 50
// If fails with Some(List(10, 10, 10, 10, 10)), shrinks to:
// None, Some(List()), Some(List(50)), Some(List(25, 25)), etc.
}
val eitherProp = forAll { (e: Either[String, Int]) =>
e.fold(_.length, identity) < 10
// Shrinks both Left values (strings) and Right values (ints)
}Automatic shrinking for tuples up to 9 elements, shrinking each component independently.
implicit def shrinkTuple2[T1, T2](
implicit s1: Shrink[T1],
s2: Shrink[T2]
): Shrink[(T1, T2)]
implicit def shrinkTuple3[T1, T2, T3](
implicit s1: Shrink[T1],
s2: Shrink[T2],
s3: Shrink[T3]
): Shrink[(T1, T2, T3)]
// ... up to Tuple9Usage Examples:
val tupleProp = forAll { (pair: (String, Int)) =>
pair._1.length + pair._2 < 20
// If fails with ("Hello", 20), shrinks both components:
// ("", 20), ("Hello", 0), ("H", 10), etc.
}
val triple = forAll { (t: (Int, List[String], Boolean)) =>
// Shrinks all three components independently
t._2.length < t._1 || !t._3
}Specialized shrinking for time-based types.
implicit val shrinkFiniteDuration: Shrink[FiniteDuration]
implicit val shrinkDuration: Shrink[Duration]Usage Examples:
val durationProp = forAll { (d: FiniteDuration) =>
d.toMillis < 1000 // Shrinks towards zero duration
}
val timeoutProp = forAll { (timeout: Duration) =>
timeout.isFinite ==> (timeout.toMillis < Long.MaxValue)
}Building domain-specific shrinking logic for custom types.
def xmap[T, U](from: T => U, to: U => T)(implicit s: Shrink[T]): Shrink[U]Usage Examples:
case class Age(years: Int)
// Transform existing shrinking strategy
implicit val shrinkAge: Shrink[Age] =
Shrink.shrinkIntegral[Int].xmap(Age(_), _.years).suchThat(_.years >= 0)
case class Email(local: String, domain: String)
// Complex custom shrinking
implicit val shrinkEmail: Shrink[Email] = Shrink { email =>
val localShrinks = Shrink.shrink(email.local).filter(_.nonEmpty)
val domainShrinks = Shrink.shrink(email.domain).filter(_.nonEmpty)
// Try shrinking local part
localShrinks.map(local => Email(local, email.domain)) ++
// Try shrinking domain part
domainShrinks.map(domain => Email(email.local, domain)) ++
// Try shrinking both
(for {
local <- localShrinks
domain <- domainShrinks
} yield Email(local, domain))
}// Use forAllNoShrink to disable shrinking for performance
val noShrinkProp = Prop.forAllNoShrink(expensiveGen) { data =>
// Property that would be slow to shrink
expensiveTest(data)
}
// Disable shrinking via test parameters
val params = Test.Parameters.default.withLegacyShrinking(true)
Test.check(someProp)(_.withLegacyShrinking(true))case class PositiveInt(value: Int)
implicit val shrinkPositiveInt: Shrink[PositiveInt] =
Shrink.shrinkIntegral[Int]
.suchThat(_ > 0) // Only shrink to positive values
.xmap(PositiveInt(_), _.value)
val constrainedProp = forAll { (pos: PositiveInt) =>
pos.value <= 0 // When this fails, only positive shrinks are tried
}sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
implicit def shrinkTree[A](implicit sa: Shrink[A]): Shrink[Tree[A]] = Shrink {
case Leaf(value) =>
sa.shrink(value).map(Leaf(_))
case Branch(left, right) =>
// Try shrinking to subtrees
Stream(left, right) ++
// Try shrinking left subtree
shrinkTree[A].shrink(left).map(Branch(_, right)) ++
// Try shrinking right subtree
shrinkTree[A].shrink(right).map(Branch(left, _)) ++
// Try shrinking both subtrees
(for {
newLeft <- shrinkTree[A].shrink(left)
newRight <- shrinkTree[A].shrink(right)
} yield Branch(newLeft, newRight))
}
val treeProp = forAll { (tree: Tree[Int]) =>
size(tree) < 100 // Shrinks tree structure and leaf values
}// ScalaCheck interleaves shrinking attempts from different strategies
// This ensures balanced exploration of the shrinking space
case class Person(name: String, age: Int, emails: List[String])
implicit val shrinkPerson: Shrink[Person] = Shrink { person =>
// Shrink each field independently
val nameShinks = Shrink.shrink(person.name).map(n => person.copy(name = n))
val ageShinks = Shrink.shrink(person.age).map(a => person.copy(age = a))
val emailShrinks = Shrink.shrink(person.emails).map(e => person.copy(emails = e))
// ScalaCheck will interleave these streams for balanced shrinking
nameShinks ++ ageShinks ++ emailShrinks
}case class SortedList[T](values: List[T])(implicit ord: Ordering[T]) {
require(values.sorted == values, "List must be sorted")
}
implicit def shrinkSortedList[T](implicit s: Shrink[T], ord: Ordering[T]): Shrink[SortedList[T]] =
Shrink { sortedList =>
// Shrink the underlying list and ensure result remains sorted
Shrink.shrink(sortedList.values)
.map(_.sorted) // Maintain invariant
.filter(_.sorted == _) // Double-check invariant
.map(SortedList(_))
}// For expensive properties, limit shrinking depth
implicit val limitedShrink: Shrink[ExpensiveData] = Shrink { data =>
// Only try first 10 shrinking attempts
expensiveDataShrinkStrategy(data).take(10)
}
// For properties with expensive generators, disable shrinking
val quickProp = Prop.forAllNoShrink(expensiveGen) { data =>
quickCheck(data)
}Install with Tessl CLI
npx tessl i tessl/maven-org-scalacheck--scalacheck-2-12