0
# Property-Based Testing
1
2
ScalaTest provides built-in support for property-based testing through generators, table-driven tests, and property checking utilities. This enables testing with automatically generated test data and verification of properties that should hold for all inputs.
3
4
## Capabilities
5
6
### Property Checks
7
8
Core trait for property-based testing that combines generator-driven and table-driven approaches.
9
10
```scala { .api }
11
trait PropertyChecks extends TableDrivenPropertyChecks with GeneratorDrivenPropertyChecks {
12
13
/**
14
* Check a property using generated values
15
*/
16
def forAll[A](gen: Generator[A])(fun: A => Assertion): Assertion
17
def forAll[A, B](genA: Generator[A], genB: Generator[B])(fun: (A, B) => Assertion): Assertion
18
def forAll[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C])(fun: (A, B, C) => Assertion): Assertion
19
20
/**
21
* Check a property using table data
22
*/
23
def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion
24
def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion
25
def forAll[A, B, C](table: TableFor3[A, B, C])(fun: (A, B, C) => Assertion): Assertion
26
27
/**
28
* Conditional property checking
29
*/
30
def whenever(condition: Boolean)(fun: => Assertion): Assertion
31
}
32
33
object PropertyChecks extends PropertyChecks
34
```
35
36
**Usage Examples:**
37
38
```scala
39
import org.scalatest.prop.PropertyChecks
40
import org.scalatest.funsuite.AnyFunSuite
41
42
class PropertyExample extends AnyFunSuite with PropertyChecks {
43
44
test("string reverse property") {
45
forAll { (s: String) =>
46
s.reverse.reverse should equal (s)
47
}
48
}
49
50
test("addition is commutative") {
51
forAll { (a: Int, b: Int) =>
52
whenever(a > 0 && b > 0) {
53
a + b should equal (b + a)
54
}
55
}
56
}
57
58
test("list concatenation properties") {
59
forAll { (list1: List[Int], list2: List[Int]) =>
60
val combined = list1 ++ list2
61
combined.length should equal (list1.length + list2.length)
62
combined.take(list1.length) should equal (list1)
63
combined.drop(list1.length) should equal (list2)
64
}
65
}
66
}
67
```
68
69
### Generators
70
71
Core generators for creating test data with shrinking support.
72
73
```scala { .api }
74
trait Generator[T] {
75
76
/**
77
* Generate the next value with shrinking support
78
*/
79
def next(szp: SizeParam, edges: List[T], rnd: Randomizer): (RoseTree[T], Randomizer)
80
81
/**
82
* Transform generated values
83
*/
84
def map[U](f: T => U): Generator[U]
85
86
/**
87
* Flat map for composing generators
88
*/
89
def flatMap[U](f: T => Generator[U]): Generator[U]
90
91
/**
92
* Filter generated values
93
*/
94
def filter(f: T => Boolean): Generator[T]
95
96
/**
97
* Create pairs of generated values
98
*/
99
def zip[U](other: Generator[U]): Generator[(T, U)]
100
101
/**
102
* Generate samples for testing
103
*/
104
def sample: Option[T]
105
def samples(n: Int): List[T]
106
}
107
108
object Generator {
109
110
/**
111
* Create generator from a function
112
*/
113
def apply[T](f: (SizeParam, List[T], Randomizer) => (RoseTree[T], Randomizer)): Generator[T]
114
115
/**
116
* Generator that always produces the same value
117
*/
118
def const[T](value: T): Generator[T]
119
120
/**
121
* Generator that chooses randomly from provided values
122
*/
123
def oneOf[T](values: T*): Generator[T]
124
def oneOf[T](gen: Generator[T], gens: Generator[T]*): Generator[T]
125
126
/**
127
* Generator that chooses from a weighted distribution
128
*/
129
def frequency[T](weightedGens: (Int, Generator[T])*): Generator[T]
130
131
/**
132
* Generator for lists with specified size range
133
*/
134
def listOfN[T](n: Int, gen: Generator[T]): Generator[List[T]]
135
def listOf[T](gen: Generator[T]): Generator[List[T]]
136
}
137
```
138
139
**Usage Examples:**
140
141
```scala
142
import org.scalatest.prop.Generator
143
144
// Custom generators
145
val evenIntGen = Generator.choose(0, 100).map(_ * 2)
146
val nonEmptyStringGen = Generator.alphaStr.filter(_.nonEmpty)
147
148
// Composed generators
149
val personGen = for {
150
name <- Generator.alphaStr.filter(_.nonEmpty)
151
age <- Generator.choose(0, 120)
152
email <- Generator.alphaStr.map(_ + "@example.com")
153
} yield Person(name, age, email)
154
155
// Using generators in tests
156
forAll(evenIntGen) { n =>
157
n % 2 should equal (0)
158
}
159
160
forAll(personGen) { person =>
161
person.name should not be empty
162
person.age should be >= 0
163
person.email should include ("@")
164
}
165
```
166
167
### Common Generators
168
169
Pre-built generators for common data types.
170
171
```scala { .api }
172
trait CommonGenerators {
173
174
// Numeric generators
175
def choose(min: Int, max: Int): Generator[Int]
176
def choose(min: Double, max: Double): Generator[Double]
177
def chooseNum[T: Numeric](min: T, max: T): Generator[T]
178
179
// String generators
180
def alphaChar: Generator[Char]
181
def alphaNumChar: Generator[Char]
182
def alphaStr: Generator[String]
183
def alphaNumStr: Generator[String]
184
def numStr: Generator[String]
185
186
// Collection generators
187
def listOf[T](gen: Generator[T]): Generator[List[T]]
188
def vectorOf[T](gen: Generator[T]): Generator[Vector[T]]
189
def setOf[T](gen: Generator[T]): Generator[Set[T]]
190
def mapOf[K, V](keyGen: Generator[K], valueGen: Generator[V]): Generator[Map[K, V]]
191
192
// Option and Either generators
193
def option[T](gen: Generator[T]): Generator[Option[T]]
194
def either[A, B](genA: Generator[A], genB: Generator[B]): Generator[Either[A, B]]
195
196
// Tuple generators
197
def tuple2[A, B](genA: Generator[A], genB: Generator[B]): Generator[(A, B)]
198
def tuple3[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C]): Generator[(A, B, C)]
199
}
200
201
object CommonGenerators extends CommonGenerators
202
```
203
204
**Usage Examples:**
205
206
```scala
207
import org.scalatest.prop.CommonGenerators._
208
209
// Using pre-built generators
210
val emailGen = for {
211
username <- alphaStr.filter(_.nonEmpty)
212
domain <- alphaStr.filter(_.nonEmpty)
213
} yield s"$username@$domain.com"
214
215
val phoneGen = numStr.map(_.take(10).padTo(10, '0'))
216
217
val addressGen = for {
218
street <- alphaNumStr
219
city <- alphaStr
220
zipCode <- numStr.map(_.take(5))
221
} yield Address(street, city, zipCode)
222
```
223
224
### Table-Driven Testing
225
226
Structured approach to testing with predefined data sets.
227
228
```scala { .api }
229
/**
230
* Table with one column of test data
231
*/
232
case class TableFor1[A](heading: String, rows: A*) extends Iterable[A] {
233
def iterator: Iterator[A] = rows.iterator
234
}
235
236
/**
237
* Table with two columns of test data
238
*/
239
case class TableFor2[A, B](heading1: String, heading2: String, rows: (A, B)*) extends Iterable[(A, B)] {
240
def iterator: Iterator[(A, B)] = rows.iterator
241
}
242
243
/**
244
* Helper for creating tables
245
*/
246
object Table {
247
def apply[A](heading: String, rows: A*): TableFor1[A] = TableFor1(heading, rows: _*)
248
def apply[A, B](heading1: String, heading2: String, rows: (A, B)*): TableFor2[A, B] =
249
TableFor2(heading1, heading2, rows: _*)
250
}
251
252
trait TableDrivenPropertyChecks {
253
/**
254
* Check property for all rows in table
255
*/
256
def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion
257
def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion
258
}
259
```
260
261
**Usage Examples:**
262
263
```scala
264
import org.scalatest.prop.{Table, TableDrivenPropertyChecks}
265
266
class TableDrivenExample extends AnyFunSuite with TableDrivenPropertyChecks {
267
268
test("mathematical operations") {
269
val examples = Table(
270
("input", "expected"),
271
(2, 4),
272
(3, 9),
273
(4, 16),
274
(5, 25)
275
)
276
277
forAll(examples) { (input, expected) =>
278
input * input should equal (expected)
279
}
280
}
281
282
test("string operations") {
283
val stringExamples = Table(
284
"input" -> "expected",
285
"hello" -> "HELLO",
286
"world" -> "WORLD",
287
"test" -> "TEST"
288
)
289
290
forAll(stringExamples) { (input, expected) =>
291
input.toUpperCase should equal (expected)
292
}
293
}
294
}
295
```
296
297
### Configuration
298
299
Configurable parameters for property-based testing.
300
301
```scala { .api }
302
trait Configuration {
303
304
/**
305
* Minimum successful tests before property passes
306
*/
307
def minSuccessful: Int = 100
308
309
/**
310
* Maximum number of discarded tests before failure
311
*/
312
def maxDiscarded: Int = 500
313
314
/**
315
* Minimum size parameter for generators
316
*/
317
def minSize: Int = 0
318
319
/**
320
* Maximum size parameter for generators
321
*/
322
def maxSize: Int = 100
323
324
/**
325
* Number of workers for parallel testing
326
*/
327
def workers: Int = 1
328
}
329
330
/**
331
* Immutable configuration for property checks
332
*/
333
case class PropertyCheckConfiguration(
334
minSuccessful: Int = 100,
335
maxDiscarded: Int = 500,
336
minSize: Int = 0,
337
maxSize: Int = 100,
338
workers: Int = 1
339
) extends Configuration
340
```
341
342
**Usage Examples:**
343
344
```scala
345
// Custom configuration
346
implicit val config = PropertyCheckConfiguration(
347
minSuccessful = 50, // Fewer required successes
348
maxDiscarded = 1000, // Allow more discarded tests
349
maxSize = 200 // Larger test data
350
)
351
352
forAll { (list: List[Int]) =>
353
list.reverse.reverse should equal (list)
354
}
355
356
// Configuration for specific test
357
forAll(PropertyCheckConfiguration(minSuccessful = 1000)) { (n: Int) =>
358
math.abs(n) should be >= 0
359
}
360
```
361
362
## Types
363
364
```scala { .api }
365
/**
366
* Random value generator with seed
367
*/
368
case class Randomizer(seed: Long) {
369
def nextInt: (Int, Randomizer)
370
def nextLong: (Long, Randomizer)
371
def nextDouble: (Double, Randomizer)
372
def nextBoolean: (Boolean, Randomizer)
373
def choose[T](values: Vector[T]): (T, Randomizer)
374
}
375
376
/**
377
* Size parameter for generators
378
*/
379
case class SizeParam(value: Int) extends AnyVal {
380
def inc: SizeParam = SizeParam(value + 1)
381
def dec: SizeParam = SizeParam(math.max(0, value - 1))
382
}
383
384
/**
385
* Tree structure for shrinking test failures
386
*/
387
trait RoseTree[+T] {
388
def value: T
389
def shrinks: Stream[RoseTree[T]]
390
def map[U](f: T => U): RoseTree[U]
391
def flatMap[U](f: T => RoseTree[U]): RoseTree[U]
392
}
393
394
object RoseTree {
395
def apply[T](value: T, shrinks: Stream[RoseTree[T]] = Stream.empty): RoseTree[T]
396
}
397
```