0
# Property-Based Testing
1
2
Sophisticated generator system for creating test data and verifying properties across input ranges. Property-based testing automatically generates test cases to verify that properties hold for a wide range of inputs.
3
4
## Capabilities
5
6
### Property Testing Functions
7
8
Core functions for running property-based tests.
9
10
```scala { .api }
11
/**
12
* Tests that a property holds for generated test data
13
* @param rv - Generator for test data
14
* @param test - Property test function
15
* @returns Effect that runs the property test
16
*/
17
def check[R <: TestConfig, A](rv: Gen[R, A])(test: A => TestResult): URIO[R, TestResult]
18
19
/**
20
* Effectful property testing
21
* @param rv - Generator for test data
22
* @param test - Effectful property test function
23
* @returns Effect that runs the property test
24
*/
25
def checkM[R <: TestConfig, R1 <: R, E, A](rv: Gen[R, A])(test: A => ZIO[R1, E, TestResult]): ZIO[R1, E, TestResult]
26
27
/**
28
* Tests property for all values from deterministic generator
29
* @param rv - Deterministic generator
30
* @param test - Property test function
31
* @returns Effect that tests all generated values
32
*/
33
def checkAll[R <: TestConfig, A](rv: Gen[R, A])(test: A => TestResult): URIO[R, TestResult]
34
35
/**
36
* Effectful exhaustive property testing
37
*/
38
def checkAllM[R <: TestConfig, R1 <: R, E, A](rv: Gen[R, A])(test: A => ZIO[R1, E, TestResult]): ZIO[R1, E, TestResult]
39
40
/**
41
* Parallel exhaustive property testing
42
* @param parallelism - Number of parallel test executions
43
*/
44
def checkAllMPar[R <: TestConfig, R1 <: R, E, A](rv: Gen[R, A], parallelism: Int)(test: A => ZIO[R1, E, TestResult]): ZIO[R1, E, TestResult]
45
```
46
47
**Multi-Parameter Property Testing:**
48
49
```scala { .api }
50
// 2-parameter versions
51
def check[R <: TestConfig, A, B](rv1: Gen[R, A], rv2: Gen[R, B])(test: (A, B) => TestResult): URIO[R, TestResult]
52
def checkM[R <: TestConfig, R1 <: R, E, A, B](rv1: Gen[R, A], rv2: Gen[R, B])(test: (A, B) => ZIO[R1, E, TestResult]): ZIO[R1, E, TestResult]
53
54
// Similar overloads exist for 3-8 parameters
55
```
56
57
**Fixed-Sample Testing:**
58
59
```scala { .api }
60
/**
61
* Tests property for exactly N samples
62
* @param n - Number of samples to test
63
* @returns CheckN instance for chaining with generator
64
*/
65
def checkN(n: Int): CheckVariants.CheckN
66
67
/**
68
* Effectful fixed-sample testing
69
*/
70
def checkNM(n: Int): CheckVariants.CheckNM
71
```
72
73
**Usage Examples:**
74
75
```scala
76
import zio.test._
77
import zio.test.Assertion._
78
79
// Basic property test
80
test("list reverse is involutive") {
81
check(Gen.listOf(Gen.anyInt)) { list =>
82
assert(list.reverse.reverse)(equalTo(list))
83
}
84
}
85
86
// Multiple generators
87
test("addition is commutative") {
88
check(Gen.anyInt, Gen.anyInt) { (a, b) =>
89
assert(a + b)(equalTo(b + a))
90
}
91
}
92
93
// Fixed sample size
94
test("custom sample size") {
95
checkN(1000)(Gen.anyInt) { n =>
96
assert(n * 0)(equalTo(0))
97
}
98
}
99
100
// Effectful property test
101
testM("database operations") {
102
checkM(Gen.alphaNumericString) { userId =>
103
for {
104
_ <- UserRepository.create(userId)
105
found <- UserRepository.find(userId)
106
} yield assert(found)(isSome)
107
}
108
}
109
```
110
111
### Generator Creation and Combinators
112
113
Core generator class and its combinators.
114
115
```scala { .api }
116
/**
117
* Generator that produces samples of type A requiring environment R
118
*/
119
final case class Gen[-R, +A](sample: ZStream[R, Nothing, Sample[R, A]]) {
120
/**
121
* Map over generated values with type transformation
122
*/
123
def map[B](f: A => B): Gen[R, B]
124
125
/**
126
* FlatMap for composing generators
127
*/
128
def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]
129
130
/**
131
* Filter generated values based on predicate
132
*/
133
def filter(f: A => Boolean): Gen[R, A]
134
135
/**
136
* Filter out values based on predicate
137
*/
138
def filterNot(f: A => Boolean): Gen[R, A]
139
140
/**
141
* Collect values using partial function
142
*/
143
def collect[B](pf: PartialFunction[A, B]): Gen[R, B]
144
145
/**
146
* Zip two generators together
147
*/
148
def zip[R1 <: R, B](that: Gen[R1, B]): Gen[R1, (A, B)]
149
150
/**
151
* Cross product of two generators (alias for zip)
152
*/
153
def <*>[R1 <: R, B](that: Gen[R1, B]): Gen[R1, (A, B)]
154
155
/**
156
* Zip with transformation function
157
*/
158
def zipWith[R1 <: R, B, C](that: Gen[R1, B])(f: (A, B) => C): Gen[R1, C]
159
}
160
```
161
162
### Primitive Generators
163
164
Built-in generators for basic types.
165
166
```scala { .api }
167
// Numeric generators
168
val anyByte: Gen[Any, Byte]
169
val anyChar: Gen[Any, Char]
170
val anyDouble: Gen[Any, Double]
171
val anyFloat: Gen[Any, Float]
172
val anyInt: Gen[Any, Int]
173
val anyLong: Gen[Any, Long]
174
175
// Bounded numeric generators
176
def byte(min: Byte, max: Byte): Gen[Any, Byte]
177
def char(min: Char, max: Char): Gen[Any, Char]
178
def double(min: Double, max: Double): Gen[Any, Double]
179
def float(min: Float, max: Float): Gen[Any, Float]
180
def int(min: Int, max: Int): Gen[Any, Int]
181
def long(min: Long, max: Long): Gen[Any, Long]
182
183
// Special numeric distributions
184
def exponential(min: Double, max: Double): Gen[Any, Double]
185
def uniform: Gen[Any, Double]
186
187
// Boolean and character generators
188
val boolean: Gen[Any, Boolean]
189
val printableChar: Gen[Any, Char]
190
val alphaChar: Gen[Any, Char]
191
val alphaNumericChar: Gen[Any, Char]
192
val hexChar: Gen[Any, Char]
193
```
194
195
**Usage Examples:**
196
197
```scala
198
// Range-based generators
199
val temperatures = Gen.double(-50.0, 50.0)
200
val dice = Gen.int(1, 6)
201
val lowercase = Gen.char('a', 'z')
202
203
// Composed generators
204
val coordinates = Gen.double(-180, 180).zip(Gen.double(-90, 90))
205
val userAge = Gen.int(13, 120).filter(_ >= 18) // Adults only
206
```
207
208
### String Generators
209
210
Generators for string types with various character sets.
211
212
```scala { .api }
213
// Basic string generators
214
val anyString: Gen[Any, String]
215
val alphaString: Gen[Sized, String]
216
val alphaNumericString: Gen[Sized, String]
217
val numericString: Gen[Sized, String]
218
219
// Custom string generators
220
def string(char: Gen[Any, Char]): Gen[Sized, String]
221
def string1(char: Gen[Any, Char]): Gen[Sized, String] // Non-empty
222
def stringN(n: Int)(char: Gen[Any, Char]): Gen[Any, String] // Fixed length
223
def stringBounded(min: Int, max: Int)(char: Gen[Any, Char]): Gen[Any, String]
224
```
225
226
**Usage Examples:**
227
228
```scala
229
// Email-like strings
230
val emailGen = for {
231
username <- Gen.stringBounded(3, 10)(Gen.alphaNumericChar)
232
domain <- Gen.stringBounded(3, 8)(Gen.alphaChar)
233
tld <- Gen.elements("com", "org", "net")
234
} yield s"$username@$domain.$tld"
235
236
// Password strings
237
val passwordGen = Gen.stringBounded(8, 20)(
238
Gen.oneOf(Gen.alphaChar, Gen.char('0', '9'), Gen.elements('!', '@', '#'))
239
)
240
```
241
242
### Collection Generators
243
244
Generators for collections with configurable sizing.
245
246
```scala { .api }
247
// List generators
248
def listOf[R, A](gen: Gen[R, A]): Gen[R with Sized, List[A]]
249
def listOf1[R, A](gen: Gen[R, A]): Gen[R with Sized, List[A]] // Non-empty
250
def listOfN[R, A](n: Int)(gen: Gen[R, A]): Gen[R, List[A]] // Fixed size
251
def listOfBounded[R, A](min: Int, max: Int)(gen: Gen[R, A]): Gen[R, List[A]]
252
253
// Set generators
254
def setOf[R, A](gen: Gen[R, A]): Gen[R with Sized, Set[A]]
255
def setOf1[R, A](gen: Gen[R, A]): Gen[R with Sized, Set[A]]
256
def setOfN[R, A](n: Int)(gen: Gen[R, A]): Gen[R, Set[A]]
257
258
// Vector generators
259
def vectorOf[R, A](gen: Gen[R, A]): Gen[R with Sized, Vector[A]]
260
def vectorOf1[R, A](gen: Gen[R, A]): Gen[R with Sized, Vector[A]]
261
def vectorOfN[R, A](n: Int)(gen: Gen[R, A]): Gen[R, Vector[A]]
262
263
// Map generators
264
def mapOf[R, A, B](key: Gen[R, A], value: Gen[R, B]): Gen[R with Sized, Map[A, B]]
265
def mapOfN[R, A, B](n: Int)(key: Gen[R, A], value: Gen[R, B]): Gen[R, Map[A, B]]
266
def mapOfBounded[R, A, B](min: Int, max: Int)(key: Gen[R, A], value: Gen[R, B]): Gen[R, Map[A, B]]
267
```
268
269
**Usage Examples:**
270
271
```scala
272
// Generate test data sets
273
val userListGen = Gen.listOfBounded(1, 100)(userGen)
274
val uniqueIdsGen = Gen.setOfN(10)(Gen.uuid)
275
val configMapGen = Gen.mapOf(Gen.alphaNumericString, Gen.anyString)
276
277
// Nested collections
278
val matrixGen = Gen.listOfN(3)(Gen.listOfN(3)(Gen.double(0, 1)))
279
```
280
281
### Choice and Frequency Generators
282
283
Generators for selecting from predefined values or weighted distributions.
284
285
```scala { .api }
286
/**
287
* Choose one value from the provided options
288
*/
289
def oneOf[R, A](as: A*): Gen[R, A]
290
291
/**
292
* Alias for oneOf
293
*/
294
def elements[A](as: A*): Gen[Any, A]
295
296
/**
297
* Choose from an iterable collection
298
*/
299
def fromIterable[A](as: Iterable[A]): Gen[Any, A]
300
301
/**
302
* Weighted choice among generators
303
* @param gs - Tuples of (generator, weight)
304
*/
305
def weighted[R, A](gs: (Gen[R, A], Double)*): Gen[R, A]
306
307
/**
308
* Frequency-based choice among generators
309
* @param gs - Tuples of (frequency, generator)
310
*/
311
def frequency[R, A](gs: (Int, Gen[R, A])*): Gen[R, A]
312
```
313
314
**Usage Examples:**
315
316
```scala
317
// Status codes
318
val httpStatusGen = Gen.elements(200, 201, 400, 401, 404, 500)
319
320
// Weighted outcomes
321
val biasedCoinGen = Gen.weighted(
322
(Gen.const(true), 0.7), // 70% heads
323
(Gen.const(false), 0.3) // 30% tails
324
)
325
326
// Error scenarios with different frequencies
327
val responseGen = Gen.frequency(
328
(8, successResponseGen), // 80% success
329
(1, clientErrorGen), // 10% client error
330
(1, serverErrorGen) // 10% server error
331
)
332
```
333
334
### Option and Either Generators
335
336
Generators for optional and error-prone values.
337
338
```scala { .api }
339
/**
340
* Generate optional values
341
*/
342
def option[R, A](gen: Gen[R, A]): Gen[R, Option[A]]
343
344
/**
345
* Generate Some values only
346
*/
347
def some[R, A](gen: Gen[R, A]): Gen[R, Some[A]]
348
349
/**
350
* Generate None values
351
*/
352
val none: Gen[Any, Option[Nothing]]
353
354
/**
355
* Generate Either values
356
*/
357
def either[R, A, B](left: Gen[R, A], right: Gen[R, B]): Gen[R, Either[A, B]]
358
```
359
360
**Usage Examples:**
361
362
```scala
363
// Optional user profiles
364
val profileGen = Gen.option(userProfileGen)
365
366
// API responses that can fail
367
val apiResponseGen = Gen.either(
368
errorMessageGen, // Left for errors
369
responseDataGen // Right for success
370
)
371
```
372
373
### Utility Generators
374
375
Special-purpose generators and utilities.
376
377
```scala { .api }
378
/**
379
* Generator that always produces the same value
380
*/
381
def const[A](a: => A): Gen[Any, A]
382
383
/**
384
* Unit generator
385
*/
386
val unit: Gen[Any, Unit]
387
388
/**
389
* Size-based generator that uses current size setting
390
*/
391
def size: Gen[Sized, Int]
392
393
/**
394
* Suspend generator evaluation
395
*/
396
def suspend[R, A](gen: => Gen[R, A]): Gen[R, A]
397
398
/**
399
* Unfold generator from initial state
400
*/
401
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]
402
```
403
404
## Types
405
406
### Core Generator Types
407
408
```scala { .api }
409
/**
410
* Generator that produces samples of type A requiring environment R
411
*/
412
final case class Gen[-R, +A](sample: ZStream[R, Nothing, Sample[R, A]])
413
414
/**
415
* A sample value with shrinking stream for property test failure minimization
416
*/
417
final case class Sample[+R, +A](
418
value: A,
419
shrink: ZStream[R, Nothing, Sample[R, A]]
420
)
421
```
422
423
### Configuration Types
424
425
```scala { .api }
426
/**
427
* Test configuration service
428
*/
429
type TestConfig = Has[TestConfig.Service]
430
431
trait TestConfig.Service {
432
def repeats: Int // Number of test repetitions
433
def retries: Int // Number of retries for flaky tests
434
def samples: Int // Number of property test samples
435
def shrinks: Int // Maximum shrinking attempts
436
}
437
438
/**
439
* Size configuration for generators
440
*/
441
type Sized = Has[Sized.Service]
442
443
trait Sized.Service {
444
def size: UIO[Int]
445
def withSize[R, E, A](size: Int)(zio: ZIO[R, E, A]): ZIO[R, E, A]
446
}
447
```
448
449
### Check Variants
450
451
```scala { .api }
452
/**
453
* Fixed-sample property testing
454
*/
455
final class CheckN(n: Int) {
456
def apply[R <: TestConfig, A](rv: Gen[R, A])(test: A => TestResult): URIO[R, TestResult]
457
// Overloads for 2-6 parameters
458
}
459
460
/**
461
* Fixed-sample effectful property testing
462
*/
463
final class CheckNM(n: Int) {
464
def apply[R <: TestConfig, R1 <: R, E, A](rv: Gen[R, A])(test: A => ZIO[R1, E, TestResult]): ZIO[R1, E, TestResult]
465
// Overloads for 2-6 parameters
466
}
467
```