CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scalacheck--scalacheck-2-12

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.

Pending
Overview
Eval results
Files

stateful-testing.mddocs/

Stateful Testing

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.

Capabilities

Core Commands Trait

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)
  )
}

Command Trait

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()

Specialized Command Types

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)
}

Command Combinators

Utilities for combining and sequencing commands.

def commandSequence(head: Command, snd: Command, rest: Command*): Command

Usage 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 sequence

Advanced Stateful Testing Patterns

Database Testing

object 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)
  }
}

Concurrent Testing

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()
}

Cache Testing

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
  }
}

Stateful Testing Best Practices

State Invariants

// 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
}

Command Generation Strategy

// 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)): _*
  )
}

Proper Resource Management

// 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

docs

arbitrary.md

cogen.md

generators.md

index.md

properties.md

property-collections.md

shrinking.md

stateful-testing.md

test-execution.md

tile.json