A comprehensive property-based testing library for Scala and Java applications that enables developers to specify program properties as testable assertions and automatically generates test cases to verify these properties.
—
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.
The fundamental framework for defining stateful testing scenarios with state machines and command sequences.
trait Commands {
type State
type Sut
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean
def newSut(state: State): Sut
def destroySut(sut: Sut): Unit
def initialPreCondition(state: State): Boolean
def genInitialState: Gen[State]
def genCommand(state: State): Gen[Command]
def property(threadCount: Int = 1, maxParComb: Int = 1000000): Prop
implicit def shrinkState: Shrink[State]
}Usage Example:
// Testing a simple counter system
object CounterCommands extends Commands {
case class State(value: Int, history: List[String])
// Mutable system under test
class Counter(initial: Int) {
private var count = initial
def increment(): Int = { count += 1; count }
def decrement(): Int = { count -= 1; count }
def get: Int = count
def reset(): Unit = { count = 0 }
}
type Sut = Counter
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
runningSuts.size < 3 // Allow up to 3 concurrent SUTs
def newSut(state: State): Sut = new Counter(state.value)
def destroySut(sut: Sut): Unit = () // No cleanup needed
def initialPreCondition(state: State): Boolean = state.value >= 0
def genInitialState: Gen[State] = Gen.choose(0, 100).map(State(_, Nil))
def genCommand(state: State): Gen[Command] = Gen.oneOf(
Gen.const(Increment),
Gen.const(Decrement),
Gen.const(Get),
Gen.const(Reset)
)
}Abstract representation of operations that can be performed on the system under test.
trait Command {
type Result
def run(sut: Sut): Result
def nextState(state: State): State
def preCondition(state: State): Boolean
def postCondition(state: State, result: Try[Result]): Prop
}Usage Examples:
// Continuing the Counter example
object CounterCommands extends Commands {
// ... previous definitions ...
case object Increment extends Command {
type Result = Int
def run(sut: Sut): Int = sut.increment()
def nextState(state: State): State =
state.copy(value = state.value + 1, history = "increment" :: state.history)
def preCondition(state: State): Boolean =
state.value < Int.MaxValue // Prevent overflow
def postCondition(state: State, result: Try[Int]): Prop = result match {
case Success(newVal) => newVal == state.value + 1
case Failure(_) => false
}
}
case object Decrement extends Command {
type Result = Int
def run(sut: Sut): Int = sut.decrement()
def nextState(state: State): State =
state.copy(value = state.value - 1, history = "decrement" :: state.history)
def preCondition(state: State): Boolean =
state.value > Int.MinValue // Prevent underflow
def postCondition(state: State, result: Try[Int]): Prop = result match {
case Success(newVal) => newVal == state.value - 1
case Failure(_) => false
}
}
case object Get extends Command {
type Result = Int
def run(sut: Sut): Int = sut.get
def nextState(state: State): State =
state.copy(history = "get" :: state.history)
def preCondition(state: State): Boolean = true // Always valid
def postCondition(state: State, result: Try[Int]): Prop = result match {
case Success(value) => value == state.value
case Failure(_) => false
}
}
case object Reset extends Command {
type Result = Unit
def run(sut: Sut): Unit = sut.reset()
def nextState(state: State): State =
State(0, "reset" :: state.history)
def preCondition(state: State): Boolean = true
def postCondition(state: State, result: Try[Unit]): Prop = result match {
case Success(_) => Prop.passed
case Failure(_) => Prop.falsified
}
}
}
// Run the stateful test
val counterProperty = CounterCommands.property()
counterProperty.check()Pre-defined command abstractions for common testing patterns.
trait SuccessCommand extends Command {
def postCondition(state: State, result: Result): Prop
}
trait UnitCommand extends Command {
type Result = Unit
def postCondition(state: State, success: Boolean): Prop
}
case object NoOp extends Command {
type Result = Unit
def run(sut: Any): Unit = ()
def nextState(state: Any): Any = state
def preCondition(state: Any): Boolean = true
def postCondition(state: Any, result: Try[Unit]): Prop = Prop.passed
}Usage Examples:
// Using SuccessCommand for operations that should never throw
case class SetValue(newValue: Int) extends SuccessCommand {
type Result = Unit
def run(sut: Counter): Unit = sut.setValue(newValue)
def nextState(state: CounterState): CounterState =
state.copy(value = newValue)
def preCondition(state: CounterState): Boolean = newValue >= 0
def postCondition(state: CounterState, result: Unit): Prop = Prop.passed
}
// Using UnitCommand for simple success/failure operations
case object Validate extends UnitCommand {
def run(sut: Counter): Unit = {
if (sut.get < 0) throw new IllegalStateException("Negative value")
}
def nextState(state: CounterState): CounterState = state
def preCondition(state: CounterState): Boolean = true
def postCondition(state: CounterState, success: Boolean): Prop =
success == (state.value >= 0)
}Utilities for combining and sequencing commands.
def commandSequence(head: Command, snd: Command, rest: Command*): CommandUsage Example:
// Create compound operations
val resetAndIncrement = commandSequence(Reset, Increment)
// This creates a command that first resets the counter, then increments it
// The postCondition ensures both operations succeeded in sequenceobject DatabaseCommands extends Commands {
case class State(records: Map[String, String], nextId: Int)
class TestDatabase {
private val storage = scala.collection.mutable.Map[String, String]()
def insert(key: String, value: String): Boolean = {
if (storage.contains(key)) false
else { storage(key) = value; true }
}
def update(key: String, value: String): Boolean = {
if (storage.contains(key)) { storage(key) = value; true }
else false
}
def delete(key: String): Boolean = storage.remove(key).isDefined
def get(key: String): Option[String] = storage.get(key)
def size: Int = storage.size
}
type Sut = TestDatabase
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
runningSuts.isEmpty // Only one database instance at a time
def newSut(state: State): Sut = {
val db = new TestDatabase
state.records.foreach { case (k, v) => db.insert(k, v) }
db
}
def destroySut(sut: Sut): Unit = () // Memory cleanup happens automatically
def initialPreCondition(state: State): Boolean = state.nextId > 0
def genInitialState: Gen[State] = Gen.const(State(Map.empty, 1))
def genCommand(state: State): Gen[Command] = {
val existingKeys = state.records.keys.toSeq
Gen.frequency(
3 -> genInsert(state),
2 -> genUpdate(existingKeys),
2 -> genDelete(existingKeys),
3 -> genGet(existingKeys)
)
}
def genInsert(state: State): Gen[Command] =
Gen.alphaStr.map(value => Insert(s"key${state.nextId}", value))
def genUpdate(keys: Seq[String]): Gen[Command] =
if (keys.nonEmpty) {
for {
key <- Gen.oneOf(keys)
value <- Gen.alphaStr
} yield Update(key, value)
} else Gen.const(NoOp)
def genDelete(keys: Seq[String]): Gen[Command] =
if (keys.nonEmpty) Gen.oneOf(keys).map(Delete)
else Gen.const(NoOp)
def genGet(keys: Seq[String]): Gen[Command] =
if (keys.nonEmpty) Gen.oneOf(keys).map(Get)
else Gen.const(NoOp)
case class Insert(key: String, value: String) extends SuccessCommand {
type Result = Boolean
def run(sut: Sut): Boolean = sut.insert(key, value)
def nextState(state: State): State =
if (state.records.contains(key)) state
else state.copy(
records = state.records + (key -> value),
nextId = state.nextId + 1
)
def preCondition(state: State): Boolean = key.nonEmpty && value.nonEmpty
def postCondition(state: State, result: Boolean): Prop =
result == !state.records.contains(key)
}
case class Update(key: String, value: String) extends SuccessCommand {
type Result = Boolean
def run(sut: Sut): Boolean = sut.update(key, value)
def nextState(state: State): State =
if (state.records.contains(key))
state.copy(records = state.records + (key -> value))
else state
def preCondition(state: State): Boolean = key.nonEmpty && value.nonEmpty
def postCondition(state: State, result: Boolean): Prop =
result == state.records.contains(key)
}
case class Delete(key: String) extends SuccessCommand {
type Result = Boolean
def run(sut: Sut): Boolean = sut.delete(key)
def nextState(state: State): State =
state.copy(records = state.records - key)
def preCondition(state: State): Boolean = key.nonEmpty
def postCondition(state: State, result: Boolean): Prop =
result == state.records.contains(key)
}
case class Get(key: String) extends SuccessCommand {
type Result = Option[String]
def run(sut: Sut): Option[String] = sut.get(key)
def nextState(state: State): State = state // Read-only operation
def preCondition(state: State): Boolean = key.nonEmpty
def postCondition(state: State, result: Option[String]): Prop =
result == state.records.get(key)
}
}object ConcurrentCommands extends Commands {
case class State(value: Int, locked: Boolean)
class ThreadSafeCounter {
private var count = 0
private val lock = new java.util.concurrent.locks.ReentrantLock()
def increment(): Int = {
lock.lock()
try {
count += 1
Thread.sleep(1) // Simulate work
count
} finally {
lock.unlock()
}
}
def get: Int = {
lock.lock()
try count finally lock.unlock()
}
}
type Sut = ThreadSafeCounter
// Allow multiple SUTs for concurrent testing
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
runningSuts.size < 10
def newSut(state: State): Sut = new ThreadSafeCounter
def destroySut(sut: Sut): Unit = ()
def initialPreCondition(state: State): Boolean = !state.locked
def genInitialState: Gen[State] = Gen.const(State(0, false))
def genCommand(state: State): Gen[Command] = Gen.oneOf(
Gen.const(ConcurrentIncrement),
Gen.const(ConcurrentGet)
)
case object ConcurrentIncrement extends SuccessCommand {
type Result = Int
def run(sut: Sut): Int = sut.increment()
def nextState(state: State): State = state.copy(value = state.value + 1)
def preCondition(state: State): Boolean = !state.locked
def postCondition(state: State, result: Int): Prop =
result > state.value // In concurrent context, result should be greater
}
case object ConcurrentGet extends SuccessCommand {
type Result = Int
def run(sut: Sut): Int = sut.get
def nextState(state: State): State = state
def preCondition(state: State): Boolean = true
def postCondition(state: State, result: Int): Prop =
result >= state.value // In concurrent context, value might have increased
}
// Test with multiple threads
val concurrentProperty = ConcurrentCommands.property(threadCount = 4)
concurrentProperty.check()
}object CacheCommands extends Commands {
case class State(data: Map[String, String], capacity: Int, evictionOrder: List[String])
class LRUCache(maxSize: Int) {
private val cache = scala.collection.mutable.LinkedHashMap[String, String]()
def put(key: String, value: String): Unit = {
if (cache.contains(key)) {
cache.remove(key)
} else if (cache.size >= maxSize) {
cache.remove(cache.head._1) // Remove least recently used
}
cache(key) = value
}
def get(key: String): Option[String] = {
cache.remove(key).map { value =>
cache(key) = value // Move to end (most recently used)
value
}
}
def size: Int = cache.size
def contains(key: String): Boolean = cache.contains(key)
}
type Sut = LRUCache
def canCreateNewSut(newState: State, initSuts: Traversable[State], runningSuts: Traversable[Sut]): Boolean =
runningSuts.isEmpty
def newSut(state: State): Sut = {
val cache = new LRUCache(state.capacity)
state.evictionOrder.reverse.foreach { key =>
state.data.get(key).foreach(cache.put(key, _))
}
cache
}
def destroySut(sut: Sut): Unit = ()
def initialPreCondition(state: State): Boolean =
state.capacity > 0 && state.data.size <= state.capacity
def genInitialState: Gen[State] =
Gen.choose(1, 5).map(cap => State(Map.empty, cap, Nil))
def genCommand(state: State): Gen[Command] = Gen.frequency(
3 -> genPut(state),
2 -> genGet(state),
1 -> Gen.const(Size)
)
def genPut(state: State): Gen[Command] =
for {
key <- Gen.alphaStr.suchThat(_.nonEmpty)
value <- Gen.alphaStr
} yield Put(key, value)
def genGet(state: State): Gen[Command] =
if (state.data.nonEmpty) Gen.oneOf(state.data.keys.toSeq).map(Get)
else Gen.const(Get("nonexistent"))
case class Put(key: String, value: String) extends SuccessCommand {
type Result = Unit
def run(sut: Sut): Unit = sut.put(key, value)
def nextState(state: State): State = {
val newData = state.data + (key -> value)
val newOrder = key :: state.evictionOrder.filterNot(_ == key)
if (newData.size > state.capacity) {
val evicted = newOrder.last
State(newData - evicted, state.capacity, newOrder.init)
} else {
State(newData, state.capacity, newOrder)
}
}
def preCondition(state: State): Boolean = key.nonEmpty
def postCondition(state: State, result: Unit): Prop = Prop.passed
}
case class Get(key: String) extends SuccessCommand {
type Result = Option[String]
def run(sut: Sut): Option[String] = sut.get(key)
def nextState(state: State): State =
if (state.data.contains(key)) {
val newOrder = key :: state.evictionOrder.filterNot(_ == key)
state.copy(evictionOrder = newOrder)
} else state
def preCondition(state: State): Boolean = key.nonEmpty
def postCondition(state: State, result: Option[String]): Prop =
result == state.data.get(key)
}
case object Size extends SuccessCommand {
type Result = Int
def run(sut: Sut): Int = sut.size
def nextState(state: State): State = state
def preCondition(state: State): Boolean = true
def postCondition(state: State, result: Int): Prop =
result == state.data.size && result <= state.capacity
}
}// Always verify state invariants in postConditions
def postCondition(state: State, result: Try[Result]): Prop = {
val basicCheck = result match {
case Success(value) => checkExpectedResult(state, value)
case Failure(ex) => checkExpectedException(state, ex)
}
val invariants = Prop.all(
state.size >= 0,
state.capacity > 0,
state.data.size <= state.capacity
)
basicCheck && invariants
}// Use state-dependent command generation
def genCommand(state: State): Gen[Command] = {
val baseCommands = List(Insert, Delete, Query)
val conditionalCommands = if (state.hasData) List(Update, Batch) else Nil
Gen.frequency(
(baseCommands ++ conditionalCommands).map(cmd => 1 -> Gen.const(cmd)): _*
)
}// Always clean up resources in destroySut
def destroySut(sut: Sut): Unit = {
sut.close()
cleanupTempFiles()
releaseNetworkConnections()
}Install with Tessl CLI
npx tessl i tessl/maven-org-scalacheck--scalacheck-2-12