0
# Test Case Shrinking
1
2
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.
3
4
## Capabilities
5
6
### Core Shrink Class
7
8
The fundamental shrinking abstraction that generates progressively smaller versions of failing inputs.
9
10
```scala { .api }
11
sealed abstract class Shrink[T] {
12
def shrink(x: T): Stream[T]
13
def suchThat(f: T => Boolean): Shrink[T]
14
}
15
16
object Shrink {
17
def apply[T](s: T => Stream[T]): Shrink[T]
18
def shrink[T](x: T)(implicit s: Shrink[T]): Stream[T]
19
def shrinkWithOrig[T](x: T)(implicit s: Shrink[T]): Stream[T]
20
}
21
```
22
23
**Usage Examples:**
24
```scala
25
// Custom shrinking strategy
26
implicit val shrinkEvenInt: Shrink[Int] = Shrink { n =>
27
if (n % 2 == 0 && n != 0) {
28
Stream(n / 2, 0) ++ Stream.from(1).take(math.abs(n) - 1).filter(_ % 2 == 0)
29
} else Stream.empty
30
}
31
32
// Filter shrunk values
33
val positiveIntShrink = Shrink.shrinkIntegral[Int].suchThat(_ > 0)
34
35
// Apply shrinking manually
36
val shrunkValues = Shrink.shrink(100) // Stream(0, 50, 75, 88, 94, 97, 99, ...)
37
```
38
39
### Default Shrinking Behavior
40
41
The default shrinking strategy that provides no shrinking for unknown types.
42
43
```scala { .api }
44
implicit def shrinkAny[T]: Shrink[T] // No shrinking by default
45
```
46
47
**Usage Examples:**
48
```scala
49
case class CustomType(value: String)
50
51
// By default, CustomType won't shrink
52
val noShrinkProp = forAll { (ct: CustomType) =>
53
ct.value.length >= 0 // If this fails, no shrinking occurs
54
}
55
56
// To enable shrinking, provide custom instance
57
implicit val shrinkCustomType: Shrink[CustomType] = Shrink { ct =>
58
Shrink.shrink(ct.value).map(CustomType(_))
59
}
60
```
61
62
### Numeric Type Shrinking
63
64
Automatic shrinking for all numeric types using mathematical reduction strategies.
65
66
```scala { .api }
67
implicit def shrinkIntegral[T](implicit num: Integral[T]): Shrink[T]
68
implicit def shrinkFractional[T](implicit num: Fractional[T]): Shrink[T]
69
```
70
71
**Usage Examples:**
72
```scala
73
val intProp = forAll { (n: Int) =>
74
n != 42 // If this fails with n=42, shrinking tries: 0, 21, 32, 37, 40, 41
75
}
76
77
val doubleProp = forAll { (d: Double) =>
78
d < 100.0 // If this fails with d=150.5, shrinking tries progressively smaller values
79
}
80
81
val bigIntProp = forAll { (bi: BigInt) =>
82
bi < BigInt(1000) // Shrinking works for arbitrary precision integers
83
}
84
```
85
86
### String Shrinking
87
88
Specialized string shrinking that reduces both length and character complexity.
89
90
```scala { .api }
91
implicit val shrinkString: Shrink[String]
92
```
93
94
**Usage Examples:**
95
```scala
96
val stringProp = forAll { (s: String) =>
97
!s.contains("bug") // If fails with "debugger", shrinks to "bug"
98
}
99
100
// String shrinking strategies:
101
// 1. Remove characters from ends and middle
102
// 2. Replace complex characters with simpler ones
103
// 3. Try empty string
104
// Example: "Hello123!" -> "Hello123", "Hello", "Hell", "H", ""
105
```
106
107
### Collection Shrinking
108
109
Automatic shrinking for all collection types, reducing both size and element complexity.
110
111
```scala { .api }
112
implicit def shrinkContainer[C[_], T](
113
implicit s: Shrink[T],
114
b: Buildable[T, C[T]]
115
): Shrink[C[T]]
116
117
implicit def shrinkContainer2[C[_, _], T, U](
118
implicit st: Shrink[T],
119
su: Shrink[U],
120
b: Buildable[(T, U), C[T, U]]
121
): Shrink[C[T, U]]
122
```
123
124
**Usage Examples:**
125
```scala
126
val listProp = forAll { (l: List[Int]) =>
127
l.sum != 100 // If fails with List(25, 25, 25, 25), shrinks to List(100), then List(50, 50), etc.
128
}
129
130
val mapProp = forAll { (m: Map[String, Int]) =>
131
m.size < 5 // Shrinks by removing entries and shrinking remaining keys/values
132
}
133
134
val setProp = forAll { (s: Set[Double]) =>
135
!s.exists(_ > 1000.0) // Shrinks set size and individual elements
136
}
137
138
// Vector, Array, Seq, and other collections automatically get shrinking
139
val vectorProp = forAll { (v: Vector[String]) =>
140
v.forall(_.length < 10) // Shrinks vector size and individual strings
141
}
142
```
143
144
### Higher-Order Type Shrinking
145
146
Shrinking strategies for Option, Either, Try, and other wrapper types.
147
148
```scala { .api }
149
implicit def shrinkOption[T](implicit s: Shrink[T]): Shrink[Option[T]]
150
implicit def shrinkEither[T1, T2](
151
implicit s1: Shrink[T1],
152
s2: Shrink[T2]
153
): Shrink[Either[T1, T2]]
154
implicit def shrinkTry[T](implicit s: Shrink[T]): Shrink[Try[T]]
155
```
156
157
**Usage Examples:**
158
```scala
159
val optionProp = forAll { (opt: Option[List[Int]]) =>
160
opt.map(_.sum).getOrElse(0) < 50
161
// If fails with Some(List(10, 10, 10, 10, 10)), shrinks to:
162
// None, Some(List()), Some(List(50)), Some(List(25, 25)), etc.
163
}
164
165
val eitherProp = forAll { (e: Either[String, Int]) =>
166
e.fold(_.length, identity) < 10
167
// Shrinks both Left values (strings) and Right values (ints)
168
}
169
```
170
171
### Tuple Shrinking
172
173
Automatic shrinking for tuples up to 9 elements, shrinking each component independently.
174
175
```scala { .api }
176
implicit def shrinkTuple2[T1, T2](
177
implicit s1: Shrink[T1],
178
s2: Shrink[T2]
179
): Shrink[(T1, T2)]
180
181
implicit def shrinkTuple3[T1, T2, T3](
182
implicit s1: Shrink[T1],
183
s2: Shrink[T2],
184
s3: Shrink[T3]
185
): Shrink[(T1, T2, T3)]
186
187
// ... up to Tuple9
188
```
189
190
**Usage Examples:**
191
```scala
192
val tupleProp = forAll { (pair: (String, Int)) =>
193
pair._1.length + pair._2 < 20
194
// If fails with ("Hello", 20), shrinks both components:
195
// ("", 20), ("Hello", 0), ("H", 10), etc.
196
}
197
198
val triple = forAll { (t: (Int, List[String], Boolean)) =>
199
// Shrinks all three components independently
200
t._2.length < t._1 || !t._3
201
}
202
```
203
204
### Duration Shrinking
205
206
Specialized shrinking for time-based types.
207
208
```scala { .api }
209
implicit val shrinkFiniteDuration: Shrink[FiniteDuration]
210
implicit val shrinkDuration: Shrink[Duration]
211
```
212
213
**Usage Examples:**
214
```scala
215
val durationProp = forAll { (d: FiniteDuration) =>
216
d.toMillis < 1000 // Shrinks towards zero duration
217
}
218
219
val timeoutProp = forAll { (timeout: Duration) =>
220
timeout.isFinite ==> (timeout.toMillis < Long.MaxValue)
221
}
222
```
223
224
### Custom Shrinking Strategies
225
226
Building domain-specific shrinking logic for custom types.
227
228
```scala { .api }
229
def xmap[T, U](from: T => U, to: U => T)(implicit s: Shrink[T]): Shrink[U]
230
```
231
232
**Usage Examples:**
233
```scala
234
case class Age(years: Int)
235
236
// Transform existing shrinking strategy
237
implicit val shrinkAge: Shrink[Age] =
238
Shrink.shrinkIntegral[Int].xmap(Age(_), _.years).suchThat(_.years >= 0)
239
240
case class Email(local: String, domain: String)
241
242
// Complex custom shrinking
243
implicit val shrinkEmail: Shrink[Email] = Shrink { email =>
244
val localShrinks = Shrink.shrink(email.local).filter(_.nonEmpty)
245
val domainShrinks = Shrink.shrink(email.domain).filter(_.nonEmpty)
246
247
// Try shrinking local part
248
localShrinks.map(local => Email(local, email.domain)) ++
249
// Try shrinking domain part
250
domainShrinks.map(domain => Email(email.local, domain)) ++
251
// Try shrinking both
252
(for {
253
local <- localShrinks
254
domain <- domainShrinks
255
} yield Email(local, domain))
256
}
257
```
258
259
## Shrinking Control and Configuration
260
261
### Disabling Shrinking
262
263
```scala
264
// Use forAllNoShrink to disable shrinking for performance
265
val noShrinkProp = Prop.forAllNoShrink(expensiveGen) { data =>
266
// Property that would be slow to shrink
267
expensiveTest(data)
268
}
269
270
// Disable shrinking via test parameters
271
val params = Test.Parameters.default.withLegacyShrinking(true)
272
Test.check(someProp)(_.withLegacyShrinking(true))
273
```
274
275
### Filtered Shrinking
276
277
```scala
278
case class PositiveInt(value: Int)
279
280
implicit val shrinkPositiveInt: Shrink[PositiveInt] =
281
Shrink.shrinkIntegral[Int]
282
.suchThat(_ > 0) // Only shrink to positive values
283
.xmap(PositiveInt(_), _.value)
284
285
val constrainedProp = forAll { (pos: PositiveInt) =>
286
pos.value <= 0 // When this fails, only positive shrinks are tried
287
}
288
```
289
290
### Recursive Data Structure Shrinking
291
292
```scala
293
sealed trait Tree[+A]
294
case class Leaf[A](value: A) extends Tree[A]
295
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
296
297
implicit def shrinkTree[A](implicit sa: Shrink[A]): Shrink[Tree[A]] = Shrink {
298
case Leaf(value) =>
299
sa.shrink(value).map(Leaf(_))
300
301
case Branch(left, right) =>
302
// Try shrinking to subtrees
303
Stream(left, right) ++
304
// Try shrinking left subtree
305
shrinkTree[A].shrink(left).map(Branch(_, right)) ++
306
// Try shrinking right subtree
307
shrinkTree[A].shrink(right).map(Branch(left, _)) ++
308
// Try shrinking both subtrees
309
(for {
310
newLeft <- shrinkTree[A].shrink(left)
311
newRight <- shrinkTree[A].shrink(right)
312
} yield Branch(newLeft, newRight))
313
}
314
315
val treeProp = forAll { (tree: Tree[Int]) =>
316
size(tree) < 100 // Shrinks tree structure and leaf values
317
}
318
```
319
320
## Shrinking Patterns and Best Practices
321
322
### Interleaved Shrinking
323
324
```scala
325
// ScalaCheck interleaves shrinking attempts from different strategies
326
// This ensures balanced exploration of the shrinking space
327
328
case class Person(name: String, age: Int, emails: List[String])
329
330
implicit val shrinkPerson: Shrink[Person] = Shrink { person =>
331
// Shrink each field independently
332
val nameShinks = Shrink.shrink(person.name).map(n => person.copy(name = n))
333
val ageShinks = Shrink.shrink(person.age).map(a => person.copy(age = a))
334
val emailShrinks = Shrink.shrink(person.emails).map(e => person.copy(emails = e))
335
336
// ScalaCheck will interleave these streams for balanced shrinking
337
nameShinks ++ ageShinks ++ emailShrinks
338
}
339
```
340
341
### Shrinking with Invariants
342
343
```scala
344
case class SortedList[T](values: List[T])(implicit ord: Ordering[T]) {
345
require(values.sorted == values, "List must be sorted")
346
}
347
348
implicit def shrinkSortedList[T](implicit s: Shrink[T], ord: Ordering[T]): Shrink[SortedList[T]] =
349
Shrink { sortedList =>
350
// Shrink the underlying list and ensure result remains sorted
351
Shrink.shrink(sortedList.values)
352
.map(_.sorted) // Maintain invariant
353
.filter(_.sorted == _) // Double-check invariant
354
.map(SortedList(_))
355
}
356
```
357
358
### Performance-Conscious Shrinking
359
360
```scala
361
// For expensive properties, limit shrinking depth
362
implicit val limitedShrink: Shrink[ExpensiveData] = Shrink { data =>
363
// Only try first 10 shrinking attempts
364
expensiveDataShrinkStrategy(data).take(10)
365
}
366
367
// For properties with expensive generators, disable shrinking
368
val quickProp = Prop.forAllNoShrink(expensiveGen) { data =>
369
quickCheck(data)
370
}
371
```