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
