CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scalacheck--scalacheck-2-12

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.

Pending
Overview
Eval results
Files

shrinking.mddocs/

Test Case Shrinking

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.

Capabilities

Core Shrink Class

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, ...)

Default Shrinking Behavior

The default shrinking strategy that provides no shrinking for unknown types.

implicit def shrinkAny[T]: Shrink[T] // No shrinking by default

Usage 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(_))
}

Numeric Type Shrinking

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
}

String Shrinking

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", ""

Collection Shrinking

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
}

Higher-Order Type Shrinking

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

Tuple Shrinking

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 Tuple9

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

Duration Shrinking

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

Custom Shrinking Strategies

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

Shrinking Control and Configuration

Disabling Shrinking

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

Filtered Shrinking

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
}

Recursive Data Structure Shrinking

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
}

Shrinking Patterns and Best Practices

Interleaved Shrinking

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

Shrinking with Invariants

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

Performance-Conscious Shrinking

// 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

docs

arbitrary.md

cogen.md

generators.md

index.md

properties.md

property-collections.md

shrinking.md

stateful-testing.md

test-execution.md

tile.json