or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

arbitrary.mdcogen.mdgenerators.mdindex.mdproperties.mdproperty-collections.mdshrinking.mdstateful-testing.mdtest-execution.md

shrinking.mddocs/

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

```