or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

async.mdfixtures.mdindex.mdmatchers.mdproperty.mdscalactic.mdtest-styles.md
tile.json

property.mddocs/

Property-Based Testing

ScalaTest provides built-in support for property-based testing through generators, table-driven tests, and property checking utilities. This enables testing with automatically generated test data and verification of properties that should hold for all inputs.

Capabilities

Property Checks

Core trait for property-based testing that combines generator-driven and table-driven approaches.

trait PropertyChecks extends TableDrivenPropertyChecks with GeneratorDrivenPropertyChecks {
  
  /**
   * Check a property using generated values
   */
  def forAll[A](gen: Generator[A])(fun: A => Assertion): Assertion
  def forAll[A, B](genA: Generator[A], genB: Generator[B])(fun: (A, B) => Assertion): Assertion
  def forAll[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C])(fun: (A, B, C) => Assertion): Assertion
  
  /**
   * Check a property using table data
   */
  def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion
  def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion
  def forAll[A, B, C](table: TableFor3[A, B, C])(fun: (A, B, C) => Assertion): Assertion
  
  /**
   * Conditional property checking
   */
  def whenever(condition: Boolean)(fun: => Assertion): Assertion
}

object PropertyChecks extends PropertyChecks

Usage Examples:

import org.scalatest.prop.PropertyChecks
import org.scalatest.funsuite.AnyFunSuite

class PropertyExample extends AnyFunSuite with PropertyChecks {
  
  test("string reverse property") {
    forAll { (s: String) =>
      s.reverse.reverse should equal (s)
    }
  }
  
  test("addition is commutative") {
    forAll { (a: Int, b: Int) =>
      whenever(a > 0 && b > 0) {
        a + b should equal (b + a)
      }
    }
  }
  
  test("list concatenation properties") {
    forAll { (list1: List[Int], list2: List[Int]) =>
      val combined = list1 ++ list2
      combined.length should equal (list1.length + list2.length)
      combined.take(list1.length) should equal (list1)
      combined.drop(list1.length) should equal (list2)
    }
  }
}

Generators

Core generators for creating test data with shrinking support.

trait Generator[T] {
  
  /**
   * Generate the next value with shrinking support
   */
  def next(szp: SizeParam, edges: List[T], rnd: Randomizer): (RoseTree[T], Randomizer)
  
  /**
   * Transform generated values
   */
  def map[U](f: T => U): Generator[U]
  
  /**
   * Flat map for composing generators
   */
  def flatMap[U](f: T => Generator[U]): Generator[U]
  
  /**
   * Filter generated values
   */
  def filter(f: T => Boolean): Generator[T]
  
  /**
   * Create pairs of generated values
   */
  def zip[U](other: Generator[U]): Generator[(T, U)]
  
  /**
   * Generate samples for testing
   */
  def sample: Option[T]
  def samples(n: Int): List[T]
}

object Generator {
  
  /**
   * Create generator from a function
   */
  def apply[T](f: (SizeParam, List[T], Randomizer) => (RoseTree[T], Randomizer)): Generator[T]
  
  /**
   * Generator that always produces the same value
   */
  def const[T](value: T): Generator[T]
  
  /**
   * Generator that chooses randomly from provided values
   */
  def oneOf[T](values: T*): Generator[T]
  def oneOf[T](gen: Generator[T], gens: Generator[T]*): Generator[T]
  
  /**
   * Generator that chooses from a weighted distribution
   */
  def frequency[T](weightedGens: (Int, Generator[T])*): Generator[T]
  
  /**
   * Generator for lists with specified size range
   */
  def listOfN[T](n: Int, gen: Generator[T]): Generator[List[T]]
  def listOf[T](gen: Generator[T]): Generator[List[T]]
}

Usage Examples:

import org.scalatest.prop.Generator

// Custom generators
val evenIntGen = Generator.choose(0, 100).map(_ * 2)
val nonEmptyStringGen = Generator.alphaStr.filter(_.nonEmpty)

// Composed generators  
val personGen = for {
  name <- Generator.alphaStr.filter(_.nonEmpty)
  age <- Generator.choose(0, 120)
  email <- Generator.alphaStr.map(_ + "@example.com")
} yield Person(name, age, email)

// Using generators in tests
forAll(evenIntGen) { n =>
  n % 2 should equal (0)
}

forAll(personGen) { person =>
  person.name should not be empty
  person.age should be >= 0
  person.email should include ("@")
}

Common Generators

Pre-built generators for common data types.

trait CommonGenerators {
  
  // Numeric generators
  def choose(min: Int, max: Int): Generator[Int]
  def choose(min: Double, max: Double): Generator[Double]
  def chooseNum[T: Numeric](min: T, max: T): Generator[T]
  
  // String generators  
  def alphaChar: Generator[Char]
  def alphaNumChar: Generator[Char]
  def alphaStr: Generator[String]
  def alphaNumStr: Generator[String]
  def numStr: Generator[String]
  
  // Collection generators
  def listOf[T](gen: Generator[T]): Generator[List[T]]
  def vectorOf[T](gen: Generator[T]): Generator[Vector[T]]
  def setOf[T](gen: Generator[T]): Generator[Set[T]]
  def mapOf[K, V](keyGen: Generator[K], valueGen: Generator[V]): Generator[Map[K, V]]
  
  // Option and Either generators
  def option[T](gen: Generator[T]): Generator[Option[T]]
  def either[A, B](genA: Generator[A], genB: Generator[B]): Generator[Either[A, B]]
  
  // Tuple generators
  def tuple2[A, B](genA: Generator[A], genB: Generator[B]): Generator[(A, B)]
  def tuple3[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C]): Generator[(A, B, C)]
}

object CommonGenerators extends CommonGenerators

Usage Examples:

import org.scalatest.prop.CommonGenerators._

// Using pre-built generators
val emailGen = for {
  username <- alphaStr.filter(_.nonEmpty)
  domain <- alphaStr.filter(_.nonEmpty)
} yield s"$username@$domain.com"

val phoneGen = numStr.map(_.take(10).padTo(10, '0'))

val addressGen = for {
  street <- alphaNumStr
  city <- alphaStr  
  zipCode <- numStr.map(_.take(5))
} yield Address(street, city, zipCode)

Table-Driven Testing

Structured approach to testing with predefined data sets.

/**
 * Table with one column of test data
 */
case class TableFor1[A](heading: String, rows: A*) extends Iterable[A] {
  def iterator: Iterator[A] = rows.iterator
}

/**
 * Table with two columns of test data
 */
case class TableFor2[A, B](heading1: String, heading2: String, rows: (A, B)*) extends Iterable[(A, B)] {
  def iterator: Iterator[(A, B)] = rows.iterator
}

/**
 * Helper for creating tables
 */
object Table {
  def apply[A](heading: String, rows: A*): TableFor1[A] = TableFor1(heading, rows: _*)
  def apply[A, B](heading1: String, heading2: String, rows: (A, B)*): TableFor2[A, B] = 
    TableFor2(heading1, heading2, rows: _*)
}

trait TableDrivenPropertyChecks {
  /**
   * Check property for all rows in table
   */
  def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion
  def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion
}

Usage Examples:

import org.scalatest.prop.{Table, TableDrivenPropertyChecks}

class TableDrivenExample extends AnyFunSuite with TableDrivenPropertyChecks {
  
  test("mathematical operations") {
    val examples = Table(
      ("input", "expected"),
      (2, 4),
      (3, 9), 
      (4, 16),
      (5, 25)
    )
    
    forAll(examples) { (input, expected) =>
      input * input should equal (expected)
    }
  }
  
  test("string operations") {
    val stringExamples = Table(
      "input" -> "expected",
      "hello" -> "HELLO",
      "world" -> "WORLD", 
      "test" -> "TEST"
    )
    
    forAll(stringExamples) { (input, expected) =>
      input.toUpperCase should equal (expected)
    }
  }
}

Configuration

Configurable parameters for property-based testing.

trait Configuration {
  
  /**
   * Minimum successful tests before property passes
   */
  def minSuccessful: Int = 100
  
  /**
   * Maximum number of discarded tests before failure
   */
  def maxDiscarded: Int = 500
  
  /**
   * Minimum size parameter for generators
   */
  def minSize: Int = 0
  
  /**
   * Maximum size parameter for generators  
   */
  def maxSize: Int = 100
  
  /**
   * Number of workers for parallel testing
   */
  def workers: Int = 1
}

/**
 * Immutable configuration for property checks
 */
case class PropertyCheckConfiguration(
  minSuccessful: Int = 100,
  maxDiscarded: Int = 500,
  minSize: Int = 0,
  maxSize: Int = 100,
  workers: Int = 1
) extends Configuration

Usage Examples:

// Custom configuration
implicit val config = PropertyCheckConfiguration(
  minSuccessful = 50,  // Fewer required successes
  maxDiscarded = 1000, // Allow more discarded tests
  maxSize = 200        // Larger test data
)

forAll { (list: List[Int]) =>
  list.reverse.reverse should equal (list)
}

// Configuration for specific test
forAll(PropertyCheckConfiguration(minSuccessful = 1000)) { (n: Int) =>
  math.abs(n) should be >= 0
}

Types

/**
 * Random value generator with seed
 */
case class Randomizer(seed: Long) {
  def nextInt: (Int, Randomizer)
  def nextLong: (Long, Randomizer)  
  def nextDouble: (Double, Randomizer)
  def nextBoolean: (Boolean, Randomizer)
  def choose[T](values: Vector[T]): (T, Randomizer)
}

/**
 * Size parameter for generators
 */
case class SizeParam(value: Int) extends AnyVal {
  def inc: SizeParam = SizeParam(value + 1)
  def dec: SizeParam = SizeParam(math.max(0, value - 1))
}

/**
 * Tree structure for shrinking test failures
 */
trait RoseTree[+T] {
  def value: T
  def shrinks: Stream[RoseTree[T]]
  def map[U](f: T => U): RoseTree[U]
  def flatMap[U](f: T => RoseTree[U]): RoseTree[U]
}

object RoseTree {
  def apply[T](value: T, shrinks: Stream[RoseTree[T]] = Stream.empty): RoseTree[T]
}