0
# Stateful Testing
1
2
ScalaCheck's Commands framework enables testing stateful systems by generating sequences of commands that operate on system-under-test instances. This approach is ideal for testing databases, caches, concurrent systems, and any stateful APIs where the history of operations affects system behavior.
3
4
## Capabilities
5
6
### Core Commands Trait
7
8
The fundamental framework for defining stateful testing scenarios with state machines and command sequences.
9
10
```scala { .api }
11
trait Commands {
12
type State
13
type Sut
14
15
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean
16
def newSut(state: State): Sut
17
def destroySut(sut: Sut): Unit
18
def initialPreCondition(state: State): Boolean
19
def genInitialState: Gen[State]
20
def genCommand(state: State): Gen[Command]
21
def property(threadCount: Int = 1, maxParComb: Int = 1000000): Prop
22
23
implicit def shrinkState: Shrink[State]
24
}
25
```
26
27
**Usage Example:**
28
```scala
29
// Testing a simple counter system
30
object CounterCommands extends Commands {
31
case class State(value: Int, history: List[String])
32
33
// Mutable system under test
34
class Counter(initial: Int) {
35
private var count = initial
36
def increment(): Int = { count += 1; count }
37
def decrement(): Int = { count -= 1; count }
38
def get: Int = count
39
def reset(): Unit = { count = 0 }
40
}
41
42
type Sut = Counter
43
44
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
45
runningSuts.size < 3 // Allow up to 3 concurrent SUTs
46
47
def newSut(state: State): Sut = new Counter(state.value)
48
49
def destroySut(sut: Sut): Unit = () // No cleanup needed
50
51
def initialPreCondition(state: State): Boolean = state.value >= 0
52
53
def genInitialState: Gen[State] = Gen.choose(0, 100).map(State(_, Nil))
54
55
def genCommand(state: State): Gen[Command] = Gen.oneOf(
56
Gen.const(Increment),
57
Gen.const(Decrement),
58
Gen.const(Get),
59
Gen.const(Reset)
60
)
61
}
62
```
63
64
### Command Trait
65
66
Abstract representation of operations that can be performed on the system under test.
67
68
```scala { .api }
69
trait Command {
70
type Result
71
def run(sut: Sut): Result
72
def nextState(state: State): State
73
def preCondition(state: State): Boolean
74
def postCondition(state: State, result: Try[Result]): Prop
75
}
76
```
77
78
**Usage Examples:**
79
```scala
80
// Continuing the Counter example
81
object CounterCommands extends Commands {
82
// ... previous definitions ...
83
84
case object Increment extends Command {
85
type Result = Int
86
87
def run(sut: Sut): Int = sut.increment()
88
89
def nextState(state: State): State =
90
state.copy(value = state.value + 1, history = "increment" :: state.history)
91
92
def preCondition(state: State): Boolean =
93
state.value < Int.MaxValue // Prevent overflow
94
95
def postCondition(state: State, result: Try[Int]): Prop = result match {
96
case Success(newVal) => newVal == state.value + 1
97
case Failure(_) => false
98
}
99
}
100
101
case object Decrement extends Command {
102
type Result = Int
103
104
def run(sut: Sut): Int = sut.decrement()
105
106
def nextState(state: State): State =
107
state.copy(value = state.value - 1, history = "decrement" :: state.history)
108
109
def preCondition(state: State): Boolean =
110
state.value > Int.MinValue // Prevent underflow
111
112
def postCondition(state: State, result: Try[Int]): Prop = result match {
113
case Success(newVal) => newVal == state.value - 1
114
case Failure(_) => false
115
}
116
}
117
118
case object Get extends Command {
119
type Result = Int
120
121
def run(sut: Sut): Int = sut.get
122
123
def nextState(state: State): State =
124
state.copy(history = "get" :: state.history)
125
126
def preCondition(state: State): Boolean = true // Always valid
127
128
def postCondition(state: State, result: Try[Int]): Prop = result match {
129
case Success(value) => value == state.value
130
case Failure(_) => false
131
}
132
}
133
134
case object Reset extends Command {
135
type Result = Unit
136
137
def run(sut: Sut): Unit = sut.reset()
138
139
def nextState(state: State): State =
140
State(0, "reset" :: state.history)
141
142
def preCondition(state: State): Boolean = true
143
144
def postCondition(state: State, result: Try[Unit]): Prop = result match {
145
case Success(_) => Prop.passed
146
case Failure(_) => Prop.falsified
147
}
148
}
149
}
150
151
// Run the stateful test
152
val counterProperty = CounterCommands.property()
153
counterProperty.check()
154
```
155
156
### Specialized Command Types
157
158
Pre-defined command abstractions for common testing patterns.
159
160
```scala { .api }
161
trait SuccessCommand extends Command {
162
def postCondition(state: State, result: Result): Prop
163
}
164
165
trait UnitCommand extends Command {
166
type Result = Unit
167
def postCondition(state: State, success: Boolean): Prop
168
}
169
170
case object NoOp extends Command {
171
type Result = Unit
172
def run(sut: Any): Unit = ()
173
def nextState(state: Any): Any = state
174
def preCondition(state: Any): Boolean = true
175
def postCondition(state: Any, result: Try[Unit]): Prop = Prop.passed
176
}
177
```
178
179
**Usage Examples:**
180
```scala
181
// Using SuccessCommand for operations that should never throw
182
case class SetValue(newValue: Int) extends SuccessCommand {
183
type Result = Unit
184
185
def run(sut: Counter): Unit = sut.setValue(newValue)
186
187
def nextState(state: CounterState): CounterState =
188
state.copy(value = newValue)
189
190
def preCondition(state: CounterState): Boolean = newValue >= 0
191
192
def postCondition(state: CounterState, result: Unit): Prop = Prop.passed
193
}
194
195
// Using UnitCommand for simple success/failure operations
196
case object Validate extends UnitCommand {
197
def run(sut: Counter): Unit = {
198
if (sut.get < 0) throw new IllegalStateException("Negative value")
199
}
200
201
def nextState(state: CounterState): CounterState = state
202
203
def preCondition(state: CounterState): Boolean = true
204
205
def postCondition(state: CounterState, success: Boolean): Prop =
206
success == (state.value >= 0)
207
}
208
```
209
210
### Command Combinators
211
212
Utilities for combining and sequencing commands.
213
214
```scala { .api }
215
def commandSequence(head: Command, snd: Command, rest: Command*): Command
216
```
217
218
**Usage Example:**
219
```scala
220
// Create compound operations
221
val resetAndIncrement = commandSequence(Reset, Increment)
222
223
// This creates a command that first resets the counter, then increments it
224
// The postCondition ensures both operations succeeded in sequence
225
```
226
227
## Advanced Stateful Testing Patterns
228
229
### Database Testing
230
231
```scala
232
object DatabaseCommands extends Commands {
233
case class State(records: Map[String, String], nextId: Int)
234
235
class TestDatabase {
236
private val storage = scala.collection.mutable.Map[String, String]()
237
238
def insert(key: String, value: String): Boolean = {
239
if (storage.contains(key)) false
240
else { storage(key) = value; true }
241
}
242
243
def update(key: String, value: String): Boolean = {
244
if (storage.contains(key)) { storage(key) = value; true }
245
else false
246
}
247
248
def delete(key: String): Boolean = storage.remove(key).isDefined
249
250
def get(key: String): Option[String] = storage.get(key)
251
252
def size: Int = storage.size
253
}
254
255
type Sut = TestDatabase
256
257
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
258
runningSuts.isEmpty // Only one database instance at a time
259
260
def newSut(state: State): Sut = {
261
val db = new TestDatabase
262
state.records.foreach { case (k, v) => db.insert(k, v) }
263
db
264
}
265
266
def destroySut(sut: Sut): Unit = () // Memory cleanup happens automatically
267
268
def initialPreCondition(state: State): Boolean = state.nextId > 0
269
270
def genInitialState: Gen[State] = Gen.const(State(Map.empty, 1))
271
272
def genCommand(state: State): Gen[Command] = {
273
val existingKeys = state.records.keys.toSeq
274
275
Gen.frequency(
276
3 -> genInsert(state),
277
2 -> genUpdate(existingKeys),
278
2 -> genDelete(existingKeys),
279
3 -> genGet(existingKeys)
280
)
281
}
282
283
def genInsert(state: State): Gen[Command] =
284
Gen.alphaStr.map(value => Insert(s"key${state.nextId}", value))
285
286
def genUpdate(keys: Seq[String]): Gen[Command] =
287
if (keys.nonEmpty) {
288
for {
289
key <- Gen.oneOf(keys)
290
value <- Gen.alphaStr
291
} yield Update(key, value)
292
} else Gen.const(NoOp)
293
294
def genDelete(keys: Seq[String]): Gen[Command] =
295
if (keys.nonEmpty) Gen.oneOf(keys).map(Delete)
296
else Gen.const(NoOp)
297
298
def genGet(keys: Seq[String]): Gen[Command] =
299
if (keys.nonEmpty) Gen.oneOf(keys).map(Get)
300
else Gen.const(NoOp)
301
302
case class Insert(key: String, value: String) extends SuccessCommand {
303
type Result = Boolean
304
305
def run(sut: Sut): Boolean = sut.insert(key, value)
306
307
def nextState(state: State): State =
308
if (state.records.contains(key)) state
309
else state.copy(
310
records = state.records + (key -> value),
311
nextId = state.nextId + 1
312
)
313
314
def preCondition(state: State): Boolean = key.nonEmpty && value.nonEmpty
315
316
def postCondition(state: State, result: Boolean): Prop =
317
result == !state.records.contains(key)
318
}
319
320
case class Update(key: String, value: String) extends SuccessCommand {
321
type Result = Boolean
322
323
def run(sut: Sut): Boolean = sut.update(key, value)
324
325
def nextState(state: State): State =
326
if (state.records.contains(key))
327
state.copy(records = state.records + (key -> value))
328
else state
329
330
def preCondition(state: State): Boolean = key.nonEmpty && value.nonEmpty
331
332
def postCondition(state: State, result: Boolean): Prop =
333
result == state.records.contains(key)
334
}
335
336
case class Delete(key: String) extends SuccessCommand {
337
type Result = Boolean
338
339
def run(sut: Sut): Boolean = sut.delete(key)
340
341
def nextState(state: State): State =
342
state.copy(records = state.records - key)
343
344
def preCondition(state: State): Boolean = key.nonEmpty
345
346
def postCondition(state: State, result: Boolean): Prop =
347
result == state.records.contains(key)
348
}
349
350
case class Get(key: String) extends SuccessCommand {
351
type Result = Option[String]
352
353
def run(sut: Sut): Option[String] = sut.get(key)
354
355
def nextState(state: State): State = state // Read-only operation
356
357
def preCondition(state: State): Boolean = key.nonEmpty
358
359
def postCondition(state: State, result: Option[String]): Prop =
360
result == state.records.get(key)
361
}
362
}
363
```
364
365
### Concurrent Testing
366
367
```scala
368
object ConcurrentCommands extends Commands {
369
case class State(value: Int, locked: Boolean)
370
371
class ThreadSafeCounter {
372
private var count = 0
373
private val lock = new java.util.concurrent.locks.ReentrantLock()
374
375
def increment(): Int = {
376
lock.lock()
377
try {
378
count += 1
379
Thread.sleep(1) // Simulate work
380
count
381
} finally {
382
lock.unlock()
383
}
384
}
385
386
def get: Int = {
387
lock.lock()
388
try count finally lock.unlock()
389
}
390
}
391
392
type Sut = ThreadSafeCounter
393
394
// Allow multiple SUTs for concurrent testing
395
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
396
runningSuts.size < 10
397
398
def newSut(state: State): Sut = new ThreadSafeCounter
399
400
def destroySut(sut: Sut): Unit = ()
401
402
def initialPreCondition(state: State): Boolean = !state.locked
403
404
def genInitialState: Gen[State] = Gen.const(State(0, false))
405
406
def genCommand(state: State): Gen[Command] = Gen.oneOf(
407
Gen.const(ConcurrentIncrement),
408
Gen.const(ConcurrentGet)
409
)
410
411
case object ConcurrentIncrement extends SuccessCommand {
412
type Result = Int
413
414
def run(sut: Sut): Int = sut.increment()
415
416
def nextState(state: State): State = state.copy(value = state.value + 1)
417
418
def preCondition(state: State): Boolean = !state.locked
419
420
def postCondition(state: State, result: Int): Prop =
421
result > state.value // In concurrent context, result should be greater
422
}
423
424
case object ConcurrentGet extends SuccessCommand {
425
type Result = Int
426
427
def run(sut: Sut): Int = sut.get
428
429
def nextState(state: State): State = state
430
431
def preCondition(state: State): Boolean = true
432
433
def postCondition(state: State, result: Int): Prop =
434
result >= state.value // In concurrent context, value might have increased
435
}
436
437
// Test with multiple threads
438
val concurrentProperty = ConcurrentCommands.property(threadCount = 4)
439
concurrentProperty.check()
440
}
441
```
442
443
### Cache Testing
444
445
```scala
446
object CacheCommands extends Commands {
447
case class State(data: Map[String, String], capacity: Int, evictionOrder: List[String])
448
449
class LRUCache(maxSize: Int) {
450
private val cache = scala.collection.mutable.LinkedHashMap[String, String]()
451
452
def put(key: String, value: String): Unit = {
453
if (cache.contains(key)) {
454
cache.remove(key)
455
} else if (cache.size >= maxSize) {
456
cache.remove(cache.head._1) // Remove least recently used
457
}
458
cache(key) = value
459
}
460
461
def get(key: String): Option[String] = {
462
cache.remove(key).map { value =>
463
cache(key) = value // Move to end (most recently used)
464
value
465
}
466
}
467
468
def size: Int = cache.size
469
def contains(key: String): Boolean = cache.contains(key)
470
}
471
472
type Sut = LRUCache
473
474
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
475
runningSuts.isEmpty
476
477
def newSut(state: State): Sut = {
478
val cache = new LRUCache(state.capacity)
479
state.evictionOrder.reverse.foreach { key =>
480
state.data.get(key).foreach(cache.put(key, _))
481
}
482
cache
483
}
484
485
def destroySut(sut: Sut): Unit = ()
486
487
def initialPreCondition(state: State): Boolean =
488
state.capacity > 0 && state.data.size <= state.capacity
489
490
def genInitialState: Gen[State] =
491
Gen.choose(1, 5).map(cap => State(Map.empty, cap, Nil))
492
493
def genCommand(state: State): Gen[Command] = Gen.frequency(
494
3 -> genPut(state),
495
2 -> genGet(state),
496
1 -> Gen.const(Size)
497
)
498
499
def genPut(state: State): Gen[Command] =
500
for {
501
key <- Gen.alphaStr.suchThat(_.nonEmpty)
502
value <- Gen.alphaStr
503
} yield Put(key, value)
504
505
def genGet(state: State): Gen[Command] =
506
if (state.data.nonEmpty) Gen.oneOf(state.data.keys.toSeq).map(Get)
507
else Gen.const(Get("nonexistent"))
508
509
case class Put(key: String, value: String) extends SuccessCommand {
510
type Result = Unit
511
512
def run(sut: Sut): Unit = sut.put(key, value)
513
514
def nextState(state: State): State = {
515
val newData = state.data + (key -> value)
516
val newOrder = key :: state.evictionOrder.filterNot(_ == key)
517
518
if (newData.size > state.capacity) {
519
val evicted = newOrder.last
520
State(newData - evicted, state.capacity, newOrder.init)
521
} else {
522
State(newData, state.capacity, newOrder)
523
}
524
}
525
526
def preCondition(state: State): Boolean = key.nonEmpty
527
528
def postCondition(state: State, result: Unit): Prop = Prop.passed
529
}
530
531
case class Get(key: String) extends SuccessCommand {
532
type Result = Option[String]
533
534
def run(sut: Sut): Option[String] = sut.get(key)
535
536
def nextState(state: State): State =
537
if (state.data.contains(key)) {
538
val newOrder = key :: state.evictionOrder.filterNot(_ == key)
539
state.copy(evictionOrder = newOrder)
540
} else state
541
542
def preCondition(state: State): Boolean = key.nonEmpty
543
544
def postCondition(state: State, result: Option[String]): Prop =
545
result == state.data.get(key)
546
}
547
548
case object Size extends SuccessCommand {
549
type Result = Int
550
551
def run(sut: Sut): Int = sut.size
552
553
def nextState(state: State): State = state
554
555
def preCondition(state: State): Boolean = true
556
557
def postCondition(state: State, result: Int): Prop =
558
result == state.data.size && result <= state.capacity
559
}
560
}
561
```
562
563
## Stateful Testing Best Practices
564
565
### State Invariants
566
567
```scala
568
// Always verify state invariants in postConditions
569
def postCondition(state: State, result: Try[Result]): Prop = {
570
val basicCheck = result match {
571
case Success(value) => checkExpectedResult(state, value)
572
case Failure(ex) => checkExpectedException(state, ex)
573
}
574
575
val invariants = Prop.all(
576
state.size >= 0,
577
state.capacity > 0,
578
state.data.size <= state.capacity
579
)
580
581
basicCheck && invariants
582
}
583
```
584
585
### Command Generation Strategy
586
587
```scala
588
// Use state-dependent command generation
589
def genCommand(state: State): Gen[Command] = {
590
val baseCommands = List(Insert, Delete, Query)
591
val conditionalCommands = if (state.hasData) List(Update, Batch) else Nil
592
593
Gen.frequency(
594
(baseCommands ++ conditionalCommands).map(cmd => 1 -> Gen.const(cmd)): _*
595
)
596
}
597
```
598
599
### Proper Resource Management
600
601
```scala
602
// Always clean up resources in destroySut
603
def destroySut(sut: Sut): Unit = {
604
sut.close()
605
cleanupTempFiles()
606
releaseNetworkConnections()
607
}
608
```