liveBook · Manning

Test case minimization

As explained in Functional Programming in Scala, Second Edition

Test case minimization reduces a failing generated input until it finds a smaller input that still causes the property to fail.

Test case minimization reduces a generated input until it finds the smallest or simplest input that still makes a property fail. Smaller counterexamples make failures easier to understand and debug. section 8.2 section 8.1

Two strategies search for a minimal failing case:

  • Shrinking runs after failure, successively decreasing the failing test case until a smaller version no longer fails; data types generally need separate shrinking code. section 8.2
  • Sized generation produces cases in increasing size and complexity, allowing the test runner to choose how it searches rather than embedding the schedule in generators. section 8.2

A revealing failing input

A property claiming that every list equals its reverse may fail on a generated list of size 10. Minimization seeks a smaller failing list, such as List(0, 1), instead of reporting the original larger case. section 8.1

Listing 8.4 Generating test cases up to a given maximum size
opaque type MaxSize = Int
object MaxSize:
  extension (x: MaxSize) def toInt: Int = x
  def fromInt(x: Int): MaxSize = x
 
opaque type Prop = (MaxSize, TestCases, RNG) => Result
 
object Prop:
  def forAll[A](g: SGen[A])(f: A => Boolean): Prop =
    (max, n, rng) =>
      val casesPerSize = (n.toInt - 1) / max.toInt + 1   #1
      val props: LazyList[Prop] =
        LazyList.from(0)
          .take((n.toInt min max.toInt) + 1)             #2
          .map(i => forAll(g(i))(f))
      val prop: Prop =
        props.map[Prop](p => (max, n, rng) =>
          p(max, casesPerSize, rng))
            .toList
            .reduce(_ && _)                              #3
      prop(max, n, rng)

A sized property distributes test cases across sizes from zero up to a maximum, using SGen values and combining the resulting properties. section 8.2

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Contrasts shrinking with sized generation and sketches SGen integration, but does not implement datatype-specific shrinking.
Examples
Uses failing list properties, a size-10 failure, and ScalaCheck’s reported List(0, 1) counterexample.
What it assumes
Assumes familiarity with properties and generated inputs while relating Gen, SGen, and Prop.
What you can do after
Diagnose property failures more effectively by interpreting minimized counterexamples and comparing shrinking with sized generation.
How it connects
Connects property-based testing with properties, generators, sized generation, and exhaustive test generation.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.