0
# Property-Based Testing
1
2
Advanced property-based testing with sophisticated generators, automatic shrinking, and configurable test execution.
3
4
## Capabilities
5
6
### Check Functions
7
8
Run property-based tests with generated inputs to verify properties hold across many test cases.
9
10
```scala { .api }
11
/**
12
* Check that a property holds for generated values from a generator
13
* @param rv generator for test values
14
* @param test property to verify for each generated value
15
* @return test result indicating success or failure with shrunk counterexamples
16
*/
17
def check[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]
18
19
/**
20
* Check property with two generators
21
*/
22
def check[R <: ZAny, A, B](rv1: Gen[R, A], rv2: Gen[R, B])(
23
test: (A, B) => TestResult
24
): ZIO[R with TestConfig, Nothing, TestResult]
25
26
/**
27
* Check property with up to 8 generators (additional overloads available)
28
*/
29
def check[R <: ZAny, A, B, C](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(
30
test: (A, B, C) => TestResult
31
): ZIO[R with TestConfig, Nothing, TestResult]
32
33
/**
34
* Check that property holds for ALL values from a finite generator
35
* @param rv finite, deterministic generator
36
* @param test property to verify
37
* @return test result
38
*/
39
def checkAll[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]
40
41
/**
42
* Check property in parallel for better performance with finite generators
43
* @param rv generator for test values
44
* @param parallelism number of parallel executions
45
* @param test property to verify
46
*/
47
def checkAllPar[R <: ZAny, A](rv: Gen[R, A], parallelism: Int)(
48
test: A => TestResult
49
): ZIO[R with TestConfig, Nothing, TestResult]
50
```
51
52
**Usage Examples:**
53
54
```scala
55
import zio.test._
56
import zio.test.Gen._
57
58
// Basic property testing
59
test("list reverse is idempotent") {
60
check(listOf(anyInt)) { list =>
61
assertTrue(list.reverse.reverse == list)
62
}
63
}
64
65
// Multiple generators
66
test("addition is commutative") {
67
check(anyInt, anyInt) { (a, b) =>
68
assertTrue(a + b == b + a)
69
}
70
}
71
72
// Effectful property testing
73
test("database round trip") {
74
check(Gen.user) { user =>
75
for {
76
saved <- Database.save(user)
77
retrieved <- Database.findById(saved.id)
78
} yield assertTrue(retrieved.contains(user))
79
}
80
}
81
82
// Exhaustive testing with finite generator
83
test("boolean operations") {
84
checkAll(Gen.boolean, Gen.boolean) { (a, b) =>
85
assertTrue((a && b) == !((!a) || (!b))) // De Morgan's law
86
}
87
}
88
```
89
90
### Gen Trait
91
92
Core generator trait for producing random values with shrinking capability.
93
94
```scala { .api }
95
/**
96
* Generator for values of type A in environment R
97
*/
98
trait Gen[+R, +A] {
99
/**
100
* Transform generated values
101
* @param f transformation function
102
* @return generator producing transformed values
103
*/
104
def map[B](f: A => B): Gen[R, B]
105
106
/**
107
* Chain generators together
108
* @param f function producing dependent generator
109
* @return generator with chained dependencies
110
*/
111
def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]
112
113
/**
114
* Filter generated values
115
* @param f predicate function
116
* @return generator producing only values satisfying predicate
117
*/
118
def filter(f: A => Boolean): Gen[R, A]
119
120
/**
121
* Generate a stream of samples with shrinking information
122
* @return stream of generated samples
123
*/
124
def sample: ZStream[R, Nothing, Sample[R, A]]
125
126
/**
127
* Combine with another generator using applicative
128
* @param that other generator
129
* @return generator producing tuples
130
*/
131
def <*>[R1 <: R, B](that: Gen[R1, B]): Gen[R1, (A, B)]
132
133
/**
134
* Generate values within specified size bounds
135
* @param min minimum size
136
* @param max maximum size
137
* @return sized generator
138
*/
139
def between(min: Int, max: Int): Gen[R with Sized, A]
140
}
141
```
142
143
### Primitive Generators
144
145
Built-in generators for basic data types.
146
147
```scala { .api }
148
// Numeric generators
149
val anyByte: Gen[Any, Byte]
150
val anyShort: Gen[Any, Short]
151
val anyInt: Gen[Any, Int]
152
val anyLong: Gen[Any, Long]
153
val anyFloat: Gen[Any, Float]
154
val anyDouble: Gen[Any, Double]
155
156
// Bounded numeric generators
157
def byte(min: Byte, max: Byte): Gen[Any, Byte]
158
def short(min: Short, max: Short): Gen[Any, Short]
159
def int(min: Int, max: Int): Gen[Any, Int]
160
def long(min: Long, max: Long): Gen[Any, Long]
161
def double(min: Double, max: Double): Gen[Any, Double]
162
163
// Character and string generators
164
val anyChar: Gen[Any, Char]
165
val anyString: Gen[Sized, String]
166
val anyASCIIString: Gen[Sized, String]
167
val alphaNumericString: Gen[Sized, String]
168
val alphaChar: Gen[Any, Char]
169
val numericChar: Gen[Any, Char]
170
171
// Boolean generator
172
val boolean: Gen[Any, Boolean]
173
174
// UUID generator
175
val anyUUID: Gen[Any, java.util.UUID]
176
```
177
178
**Usage Examples:**
179
180
```scala
181
import zio.test.Gen._
182
183
// Basic generators
184
test("numeric properties") {
185
check(anyInt) { n =>
186
assertTrue(n + 0 == n)
187
}
188
}
189
190
// Bounded generators
191
test("percentage calculations") {
192
check(double(0.0, 100.0)) { percentage =>
193
assertTrue(percentage >= 0.0 && percentage <= 100.0)
194
}
195
}
196
197
// String generators
198
test("string length properties") {
199
check(anyString) { str =>
200
assertTrue(str.length >= 0 && str.reverse.length == str.length)
201
}
202
}
203
```
204
205
### Collection Generators
206
207
Generators for collections with configurable sizes.
208
209
```scala { .api }
210
/**
211
* Generate lists of specified size
212
* @param n exact size of generated lists
213
* @param g generator for list elements
214
* @return generator producing lists of size n
215
*/
216
def listOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, List[A]]
217
218
/**
219
* Generate lists with sizes bounded by current Sized environment
220
* @param g generator for list elements
221
* @return generator producing variable-sized lists
222
*/
223
def listOf[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]
224
225
/**
226
* Generate lists with size between min and max
227
* @param min minimum list size
228
* @param max maximum list size
229
* @param g generator for list elements
230
*/
231
def listOfBounded[R, A](min: Int, max: Int)(g: Gen[R, A]): Gen[R, List[A]]
232
233
// Vector generators
234
def vectorOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Vector[A]]
235
def vectorOf[R, A](g: Gen[R, A]): Gen[R with Sized, Vector[A]]
236
237
// Set generators (automatically deduplicates)
238
def setOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Set[A]]
239
def setOf[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]
240
241
// Map generators
242
def mapOfN[R, A, B](n: Int)(g: Gen[R, (A, B)]): Gen[R, Map[A, B]]
243
def mapOf[R, A, B](g: Gen[R, (A, B)]): Gen[R with Sized, Map[A, B]]
244
245
// Non-empty collections
246
def listOf1[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]
247
def setOf1[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]
248
```
249
250
**Usage Examples:**
251
252
```scala
253
import zio.test.Gen._
254
255
// Fixed-size collections
256
test("list operations") {
257
check(listOfN(5)(anyInt)) { list =>
258
assertTrue(list.size == 5 && list.reverse.reverse == list)
259
}
260
}
261
262
// Variable-size collections
263
test("set properties") {
264
check(setOf(anyInt)) { set =>
265
assertTrue(set.union(set) == set && set.intersect(set) == set)
266
}
267
}
268
269
// Non-empty collections
270
test("head and tail operations") {
271
check(listOf1(anyString)) { list =>
272
assertTrue(list.nonEmpty && list.head :: list.tail == list)
273
}
274
}
275
276
// Maps
277
test("map properties") {
278
check(mapOf(anyString.zip(anyInt))) { map =>
279
assertTrue(map.keys.toSet.size <= map.size)
280
}
281
}
282
```
283
284
### Generator Combinators
285
286
Functions for combining and transforming generators.
287
288
```scala { .api }
289
/**
290
* Choose randomly from provided generators
291
* @param first first generator option
292
* @param rest additional generator options
293
* @return generator that randomly selects from provided options
294
*/
295
def oneOf[R, A](first: Gen[R, A], rest: Gen[R, A]*): Gen[R, A]
296
297
/**
298
* Create generator from effectful computation
299
* @param effect ZIO effect producing values
300
* @return generator that runs the effect
301
*/
302
def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]
303
304
/**
305
* Generate values using unfold pattern
306
* @param s initial state
307
* @param f function from state to next state and value
308
* @return generator using unfold pattern
309
*/
310
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]
311
312
/**
313
* Choose from generators with specified weights
314
* @param generators weighted generator options
315
* @return generator that selects based on weights
316
*/
317
def weighted[R, A](generators: (Gen[R, A], Double)*): Gen[R, A]
318
319
/**
320
* Generate constant value
321
* @param a constant value to generate
322
* @return generator always producing the constant
323
*/
324
def const[A](a: => A): Gen[Any, A]
325
326
/**
327
* Generate from explicit list of values
328
* @param first first value option
329
* @param rest additional value options
330
* @return generator randomly selecting from provided values
331
*/
332
def elements[A](first: A, rest: A*): Gen[Any, A]
333
334
/**
335
* Generate optional values (Some or None)
336
* @param g generator for Some values
337
* @return generator producing Option[A]
338
*/
339
def option[R, A](g: Gen[R, A]): Gen[R, Option[A]]
340
341
/**
342
* Generate Either values
343
* @param left generator for Left values
344
* @param right generator for Right values
345
* @return generator producing Either[A, B]
346
*/
347
def either[R, A, B](left: Gen[R, A], right: Gen[R, B]): Gen[R, Either[A, B]]
348
349
/**
350
* Generate tuples
351
* @param g1 generator for first element
352
* @param g2 generator for second element
353
* @return generator producing tuples
354
*/
355
def zip[R, A, B](g1: Gen[R, A], g2: Gen[R, B]): Gen[R, (A, B)]
356
```
357
358
**Usage Examples:**
359
360
```scala
361
import zio.test.Gen._
362
363
// Choose from generators
364
val stringOrInt: Gen[Any, Any] = oneOf(anyString, anyInt)
365
366
// Weighted choice
367
val mostlyPositive: Gen[Any, Int] = weighted(
368
int(1, 100) -> 0.8,
369
int(-100, 0) -> 0.2
370
)
371
372
// Constant and elements
373
val httpMethods = elements("GET", "POST", "PUT", "DELETE")
374
val alwaysTrue = const(true)
375
376
// Option and Either
377
test("optional values") {
378
check(option(anyInt)) { optInt =>
379
optInt match {
380
case Some(n) => assertTrue(n.toString.nonEmpty)
381
case None => assertTrue(true)
382
}
383
}
384
}
385
386
test("either values") {
387
check(either(anyString, anyInt)) { stringOrInt =>
388
stringOrInt match {
389
case Left(s) => assertTrue(s.isInstanceOf[String])
390
case Right(n) => assertTrue(n.isInstanceOf[Int])
391
}
392
}
393
}
394
```
395
396
### Custom Generators
397
398
Building domain-specific generators for complex data types.
399
400
```scala { .api }
401
/**
402
* Create generator that may fail to produce values
403
* @param pf partial function defining generation logic
404
* @return generator with filtering logic
405
*/
406
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]
407
408
/**
409
* Create recursive generators with size control
410
* @param base base case generator for small sizes
411
* @param rec recursive case for larger sizes
412
* @return size-aware recursive generator
413
*/
414
def sized[R, A](f: Int => Gen[R, A]): Gen[R with Sized, A]
415
416
/**
417
* Generate from a ZIO effect
418
* @param effect effectful value generation
419
* @return generator wrapping the effect
420
*/
421
def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]
422
```
423
424
**Usage Examples:**
425
426
```scala
427
import zio.test.Gen._
428
429
// Custom data types
430
case class User(id: Int, name: String, email: String, age: Int)
431
432
val genUser: Gen[Any, User] = for {
433
id <- int(1, 10000)
434
name <- elements("Alice", "Bob", "Charlie", "Diana")
435
domain <- elements("example.com", "test.org", "demo.net")
436
age <- int(18, 80)
437
} yield User(id, name, s"${name.toLowerCase}@$domain", age)
438
439
// Recursive data structures
440
sealed trait Tree[A]
441
case class Leaf[A](value: A) extends Tree[A]
442
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
443
444
def genTree[A](genA: Gen[Any, A]): Gen[Sized, Tree[A]] = {
445
val genLeaf = genA.map(Leaf(_))
446
val genBranch = for {
447
left <- genTree(genA)
448
right <- genTree(genA)
449
} yield Branch(left, right)
450
451
sized { size =>
452
if (size <= 1) genLeaf
453
else oneOf(genLeaf, genBranch)
454
}
455
}
456
457
// Using custom generators
458
test("user validation") {
459
check(genUser) { user =>
460
assertTrue(
461
user.id > 0 &&
462
user.name.nonEmpty &&
463
user.email.contains("@") &&
464
user.age >= 18
465
)
466
}
467
}
468
```
469
470
## Sample Type
471
472
Represents generated values with shrinking information for counterexample minimization.
473
474
```scala { .api }
475
/**
476
* A generated sample with shrinking capability
477
*/
478
trait Sample[+R, +A] {
479
/**
480
* The generated value
481
*/
482
def value: A
483
484
/**
485
* Stream of shrunk variants of this sample
486
* @return stream of smaller samples for counterexample minimization
487
*/
488
def shrink: ZStream[R, Nothing, Sample[R, A]]
489
490
/**
491
* Transform the sample value
492
* @param f transformation function
493
* @return sample with transformed value
494
*/
495
def map[B](f: A => B): Sample[R, B]
496
497
/**
498
* Transform with environment modification
499
* @param f environment transformation
500
* @return sample in transformed environment
501
*/
502
def mapZIO[R1, B](f: A => ZIO[R1, Nothing, B]): Sample[R1, B]
503
}
504
```
505
506
## Sized Environment
507
508
Controls the size of generated collections and data structures.
509
510
```scala { .api }
511
/**
512
* Environment service that provides size bounds for generators
513
*/
514
case class Sized(size: Int) extends AnyVal
515
516
object Sized {
517
/**
518
* Create a Sized service with fixed size
519
* @param size the size value
520
* @return layer providing Sized service
521
*/
522
def live(size: Int): ULayer[Sized]
523
524
/**
525
* Access current size from environment
526
*/
527
val size: URIO[Sized, Int]
528
}
529
```
530
531
**Usage Examples:**
532
533
```scala
534
import zio.test._
535
536
// Control generation size
537
test("large collections").provideLayer(Sized.live(1000)) {
538
check(listOf(anyInt)) { largeList =>
539
assertTrue(largeList.size <= 1000)
540
}
541
}
542
543
// Size-aware generators
544
val genSizedString: Gen[Sized, String] = sized { size =>
545
listOfN(size)(alphaChar).map(_.mkString)
546
}
547
```