0
# Property Collections
1
2
ScalaCheck's Properties framework enables organizing related properties into testable suites with command-line runners, batch execution support, and hierarchical organization. Property collections provide structure for large test suites and enable systematic testing of complex systems.
3
4
## Capabilities
5
6
### Core Properties Class
7
8
The fundamental class for organizing and executing collections of named properties.
9
10
```scala { .api }
11
open class Properties(val name: String) {
12
val name: String
13
def properties: Seq[(String, Prop)]
14
def check(prms: Test.Parameters = Test.Parameters.default): Unit
15
def main(args: Array[String]): Unit
16
def include(ps: Properties): Unit
17
def include(ps: Properties, prefix: String): Unit
18
def overrideParameters(p: Test.Parameters): Test.Parameters
19
}
20
```
21
22
**Usage Examples:**
23
```scala
24
object MathProperties extends Properties("Mathematics") {
25
property("addition commutative") = forAll { (a: Int, b: Int) =>
26
a + b == b + a
27
}
28
29
property("multiplication identity") = forAll { (a: Int) =>
30
a * 1 == a
31
}
32
33
property("division by self") = forAll { (a: Int) =>
34
(a != 0) ==> (a / a == 1)
35
}
36
}
37
38
// Check all properties
39
MathProperties.check()
40
41
// Run from command line
42
MathProperties.main(Array("--verbosity", "2"))
43
```
44
45
### Property Specification
46
47
The mechanism for adding properties to collections using assignment syntax.
48
49
```scala { .api }
50
sealed class PropertySpecifier {
51
def update(propName: String, p: => Prop): Unit
52
}
53
54
val property: PropertySpecifier
55
```
56
57
**Usage Examples:**
58
```scala
59
object StringProperties extends Properties("String Operations") {
60
// Using property specifier with assignment syntax
61
property("reverse twice") = forAll { (s: String) =>
62
s.reverse.reverse == s
63
}
64
65
property("concatenation length") = forAll { (s1: String, s2: String) =>
66
(s1 + s2).length == s1.length + s2.length
67
}
68
69
property("empty string identity") = forAll { (s: String) =>
70
s + "" == s && "" + s == s
71
}
72
}
73
```
74
75
### Property Specification with Seeds
76
77
Advanced property specification that allows explicit seed control for reproducible testing.
78
79
```scala { .api }
80
sealed class PropertyWithSeedSpecifier {
81
def update(propName: String, optSeed: Option[String], p: => Prop): Unit
82
}
83
84
val propertyWithSeed: PropertyWithSeedSpecifier
85
```
86
87
**Usage Examples:**
88
```scala
89
object ReproducibleTests extends Properties("Reproducible") {
90
// Property with explicit seed for reproducibility
91
propertyWithSeed("deterministic test", Some("SGVsbG8gV29ybGQ")) = forAll { (x: Int) =>
92
x + 0 == x
93
}
94
95
// Property without seed (will use random seed but show it on failure)
96
propertyWithSeed("debug test", None) = forAll { (data: ComplexData) =>
97
processData(data).isValid
98
}
99
}
100
```
101
102
### Property Collection Inclusion
103
104
Mechanism for composing larger test suites from smaller property collections.
105
106
```scala { .api }
107
def include(ps: Properties): Unit
108
def include(ps: Properties, prefix: String): Unit
109
```
110
111
**Usage Examples:**
112
```scala
113
object CoreLogic extends Properties("Core") {
114
property("basic invariant") = forAll { (x: Int) => x == x }
115
}
116
117
object AdvancedLogic extends Properties("Advanced") {
118
property("complex invariant") = forAll { (data: ComplexType) =>
119
validateComplex(data)
120
}
121
}
122
123
object AllTests extends Properties("Complete Test Suite") {
124
// Include other property collections
125
include(CoreLogic)
126
include(AdvancedLogic, "advanced.")
127
128
// Additional properties specific to this suite
129
property("integration test") = forAll { (a: Int, b: String) =>
130
integrate(a, b).nonEmpty
131
}
132
}
133
134
// This will run all properties from included collections plus local properties
135
AllTests.check()
136
```
137
138
### Parameter Customization
139
140
Override default test parameters for specific property collections.
141
142
```scala { .api }
143
def overrideParameters(p: Test.Parameters): Test.Parameters
144
```
145
146
**Usage Examples:**
147
```scala
148
object PerformanceTests extends Properties("Performance") {
149
// Custom parameters for this collection
150
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
151
p.withMinSuccessfulTests(10000)
152
.withWorkers(4)
153
.withMaxSize(1000)
154
}
155
156
property("large data processing") = forAll { (data: List[Int]) =>
157
processLargeDataset(data).size <= data.size
158
}
159
}
160
161
object QuickTests extends Properties("Quick Smoke Tests") {
162
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
163
p.withMinSuccessfulTests(10)
164
.withMaxSize(20)
165
}
166
167
property("smoke test") = forAll { (x: Int) => x + 1 > x }
168
}
169
```
170
171
### Command-Line Execution
172
173
Built-in command-line runner with parameter parsing and result reporting.
174
175
```scala { .api }
176
def main(args: Array[String]): Unit
177
```
178
179
**Usage Examples:**
180
```scala
181
object CommandLineTests extends Properties("CLI Tests") {
182
property("always passes") = Prop.passed
183
property("sometimes fails") = forAll { (x: Int) => x != 42 }
184
}
185
186
// Command line usage:
187
// scala CommandLineTests --minSuccessfulTests 1000 --workers 4
188
// scala CommandLineTests --verbosity 2 --maxDiscardRatio 10
189
190
// In application code:
191
CommandLineTests.main(Array(
192
"--minSuccessfulTests", "500",
193
"--workers", "2",
194
"--verbosity", "1"
195
))
196
```
197
198
## Property Organization Patterns
199
200
### Hierarchical Test Organization
201
202
```scala
203
// Domain-specific property collections
204
object UserManagement extends Properties("User Management") {
205
property("user creation") = forAll { (name: String, email: String) =>
206
val user = createUser(name, email)
207
user.name == name && user.email == email
208
}
209
210
property("user validation") = forAll { (user: User) =>
211
validateUser(user).isValid ==> user.isComplete
212
}
213
}
214
215
object OrderProcessing extends Properties("Order Processing") {
216
property("order total calculation") = forAll { (items: List[OrderItem]) =>
217
val order = Order(items)
218
order.total >= 0 && order.total == items.map(_.price).sum
219
}
220
}
221
222
object DatabaseOperations extends Properties("Database") {
223
property("CRUD operations") = forAll { (entity: Entity) =>
224
val saved = database.save(entity)
225
val retrieved = database.findById(saved.id)
226
retrieved.contains(saved)
227
}
228
}
229
230
// Top-level test suite
231
object ApplicationTestSuite extends Properties("E-Commerce Application") {
232
include(UserManagement, "users.")
233
include(OrderProcessing, "orders.")
234
include(DatabaseOperations, "db.")
235
236
property("end-to-end workflow") = forAll { (user: User, items: List[Item]) =>
237
val order = createOrder(user, items)
238
processPayment(order).isSuccess ==> order.status == OrderStatus.Paid
239
}
240
}
241
```
242
243
### Environment-Specific Testing
244
245
```scala
246
trait TestEnvironment {
247
def databaseUrl: String
248
def apiEndpoint: String
249
}
250
251
object LocalTestEnv extends TestEnvironment {
252
def databaseUrl = "jdbc:h2:mem:test"
253
def apiEndpoint = "http://localhost:8080"
254
}
255
256
object StagingTestEnv extends TestEnvironment {
257
def databaseUrl = "jdbc:postgresql://staging-db:5432/app"
258
def apiEndpoint = "https://staging-api.example.com"
259
}
260
261
abstract class EnvironmentProperties(name: String, env: TestEnvironment) extends Properties(name) {
262
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
263
p.withTestCallback(ConsoleReporter(1))
264
.withMinSuccessfulTests(if (env == LocalTestEnv) 100 else 1000)
265
}
266
}
267
268
object LocalTests extends EnvironmentProperties("Local Tests", LocalTestEnv) {
269
property("database connection") = Prop {
270
connectToDatabase(LocalTestEnv.databaseUrl).isSuccess
271
}
272
}
273
274
object StagingTests extends EnvironmentProperties("Staging Tests", StagingTestEnv) {
275
property("api availability") = Prop {
276
httpGet(StagingTestEnv.apiEndpoint + "/health").status == 200
277
}
278
}
279
```
280
281
### Property Categorization
282
283
```scala
284
object PropertyCategories {
285
// Fast unit tests
286
object UnitTests extends Properties("Unit Tests") {
287
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
288
p.withMinSuccessfulTests(100).withMaxSize(50)
289
}
290
291
property("string operations") = forAll { (s: String) =>
292
s.toLowerCase.toUpperCase.toLowerCase == s.toLowerCase
293
}
294
}
295
296
// Slower integration tests
297
object IntegrationTests extends Properties("Integration Tests") {
298
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
299
p.withMinSuccessfulTests(50).withWorkers(2)
300
}
301
302
property("service integration") = forAll { (request: ServiceRequest) =>
303
val response = callExternalService(request)
304
response.isValid && response.correlationId == request.id
305
}
306
}
307
308
// Performance-focused tests
309
object PerformanceTests extends Properties("Performance Tests") {
310
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
311
p.withMinSuccessfulTests(1000)
312
.withMaxSize(10000)
313
.withWorkers(Runtime.getRuntime.availableProcessors())
314
}
315
316
property("large data processing") = forAll { (data: List[DataRecord]) =>
317
val start = System.currentTimeMillis()
318
val result = processLargeDataset(data)
319
val duration = System.currentTimeMillis() - start
320
321
result.size == data.size && duration < 5000 // Under 5 seconds
322
}
323
}
324
}
325
326
// Master suite that includes all categories
327
object AllTests extends Properties("Complete Test Suite") {
328
include(PropertyCategories.UnitTests, "unit.")
329
include(PropertyCategories.IntegrationTests, "integration.")
330
include(PropertyCategories.PerformanceTests, "performance.")
331
}
332
```
333
334
### Test Result Analysis
335
336
```scala
337
object TestAnalyzer extends Properties("Analyzer") {
338
property("analyzed property") = forAll { (data: TestData) =>
339
classify(data.size == 0, "empty") {
340
classify(data.size < 10, "small", "large") {
341
collect(data.category) {
342
processTestData(data).isValid
343
}
344
}
345
}
346
}
347
}
348
349
// Custom test runner with result analysis
350
object CustomTestRunner {
351
def runWithAnalysis(props: Properties): Unit = {
352
val results = Test.checkProperties(
353
Test.Parameters.default.withTestCallback(ConsoleReporter(2)),
354
props
355
)
356
357
val (passed, failed) = results.partition(_._2.passed)
358
359
println(s"\n=== Test Results ===")
360
println(s"Passed: ${passed.size}")
361
println(s"Failed: ${failed.size}")
362
363
if (failed.nonEmpty) {
364
println(s"\nFailed Properties:")
365
failed.foreach { case (name, result) =>
366
println(s" ✗ $name")
367
result.status match {
368
case Test.Failed(args, labels) =>
369
println(s" Args: ${args.map(_.arg).mkString(", ")}")
370
if (labels.nonEmpty) println(s" Labels: ${labels.mkString(", ")}")
371
case Test.PropException(_, ex, _) =>
372
println(s" Exception: ${ex.getMessage}")
373
case _ =>
374
}
375
}
376
}
377
378
// Analyze collected data
379
results.foreach { case (name, result) =>
380
if (result.freqMap.total > 0) {
381
println(s"\n$name - Collected Data:")
382
result.freqMap.getRatios.take(5).foreach { case (item, ratio) =>
383
println(f" $item: ${ratio * 100}%.1f%%")
384
}
385
}
386
}
387
}
388
}
389
390
// Usage
391
CustomTestRunner.runWithAnalysis(ApplicationTestSuite)
392
```
393
394
### Conditional Property Execution
395
396
```scala
397
object ConditionalTests extends Properties("Conditional Tests") {
398
// Only run expensive tests if environment variable is set
399
if (sys.env.get("RUN_EXPENSIVE_TESTS").contains("true")) {
400
property("expensive computation") = forAll { (data: LargeDataSet) =>
401
expensiveComputation(data).isOptimal
402
}
403
}
404
405
// Platform-specific tests
406
if (System.getProperty("os.name").toLowerCase.contains("linux")) {
407
property("linux-specific feature") = forAll { (path: String) =>
408
linuxSpecificOperation(path).isSuccess
409
}
410
}
411
412
// Database-dependent tests
413
try {
414
connectToTestDatabase()
415
property("database operations") = forAll { (entity: Entity) =>
416
saveToDatabase(entity).isSuccess
417
}
418
} catch {
419
case _: Exception =>
420
println("Skipping database tests - database not available")
421
}
422
}
423
```
424
425
### Nested Property Collections
426
427
```scala
428
abstract class ModuleTests(moduleName: String) extends Properties(s"$moduleName Module") {
429
def unitTests: Properties
430
def integrationTests: Properties
431
432
include(unitTests, "unit.")
433
include(integrationTests, "integration.")
434
}
435
436
object AuthenticationModule extends ModuleTests("Authentication") {
437
object unitTests extends Properties("Auth Unit Tests") {
438
property("password hashing") = forAll { (password: String) =>
439
val hashed = hashPassword(password)
440
hashed != password && verifyPassword(password, hashed)
441
}
442
}
443
444
object integrationTests extends Properties("Auth Integration Tests") {
445
property("login flow") = forAll { (credentials: LoginCredentials) =>
446
val token = authenticate(credentials)
447
token.isValid ==> validateToken(token).isSuccess
448
}
449
}
450
}
451
452
object PaymentModule extends ModuleTests("Payment") {
453
object unitTests extends Properties("Payment Unit Tests") {
454
property("amount calculations") = forAll { (amount: BigDecimal, tax: Double) =>
455
val total = calculateTotal(amount, tax)
456
total >= amount && total > 0
457
}
458
}
459
460
object integrationTests extends Properties("Payment Integration Tests") {
461
property("payment processing") = forAll { (payment: PaymentRequest) =>
462
val result = processPayment(payment)
463
result.status == PaymentStatus.Success ==> result.transactionId.nonEmpty
464
}
465
}
466
}
467
468
object ApplicationModules extends Properties("All Modules") {
469
include(AuthenticationModule)
470
include(PaymentModule)
471
472
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
473
p.withMinSuccessfulTests(200).withWorkers(3)
474
}
475
}
476
```