or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

arbitrary.mdcogen.mdgenerators.mdindex.mdproperties.mdproperty-collections.mdshrinking.mdstateful-testing.mdtest-execution.md

stateful-testing.mddocs/

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

```