or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assertions.mdcore-testing.mdindex.mdproperty-testing.mdsmart-assertions.mdtest-aspects.mdtest-services.md
tile.json

property-testing.mddocs/

Property-Based Testing

Generator system for creating random test data and property-based testing with automatic shrinking support. Property-based testing validates code against general properties rather than specific examples.

Capabilities

Core Generator Type

The fundamental generator type for producing test data.

/**
 * A generator of values of type A requiring environment R
 * @tparam R - Environment type required for generation
 * @tparam A - Type of values to generate
 */
case class Gen[-R, +A](sample: ZStream[R, Nothing, Sample[R, A]]) {
  /** Transform generated values */
  def map[B](f: A => B)(implicit trace: Trace): Gen[R, B]
  
  /** Effectfully transform generated values */
  def mapZIO[R1 <: R, B](f: A => ZIO[R1, Nothing, B])(implicit trace: Trace): Gen[R1, B]
  
  /** Monadic composition of generators */
  def flatMap[R1 <: R, B](f: A => Gen[R1, B])(implicit trace: Trace): Gen[R1, B]
  
  /** Filter generated values by predicate */
  def filter(f: A => Boolean)(implicit trace: Trace): Gen[R, A]
  
  /** Effectfully filter generated values */
  def filterZIO[R1 <: R](f: A => ZIO[R1, Nothing, Boolean])(implicit trace: Trace): Gen[R1, A]
  
  /** Collect values using partial function */
  def collect[B](pf: PartialFunction[A, B])(implicit trace: Trace): Gen[R, B]
  
  /** Combine two generators */
  def zip[R1 <: R, B](that: Gen[R1, B])(implicit trace: Trace): Gen[R1, (A, B)]
  
  /** Combine generators with custom function */
  def zipWith[R1 <: R, B, C](that: Gen[R1, B])(f: (A, B) => C)(implicit trace: Trace): Gen[R1, C]
  
  /** Concatenate with another generator */
  def concat[R1 <: R, A1 >: A](that: Gen[R1, A1])(implicit trace: Trace): Gen[R1, A1]
  
  /** Disable shrinking for this generator */
  def noShrink(implicit trace: Trace): Gen[R, A]
  
  /** Replace shrinking strategy */
  def reshrink[R1 <: R](f: A => ZStream[R1, Nothing, A])(implicit trace: Trace): Gen[R1, A]
  
  /** Set size parameter for generator */
  def resize(size: Int)(implicit trace: Trace): Gen[R, A]
}

Sample Type

Container for generated values with shrinking information.

/**
 * A sample contains a generated value and shrinking information
 * @tparam R - Environment type
 * @tparam A - Value type
 */
case class Sample[-R, +A](value: A, shrink: ZStream[R, Nothing, Sample[R, A]]) {
  /** Transform the sample value */
  def map[B](f: A => B): Sample[R, B]
  
  /** Apply function to each element in shrink tree */
  def foreach[R1 <: R, B](f: A => ZIO[R1, Nothing, B])(implicit trace: Trace): ZIO[R1, Nothing, Sample[R1, B]]
  
  /** Search through shrinks for values matching predicate */
  def shrinkSearch(f: A => Boolean)(implicit trace: Trace): ZStream[R, Nothing, A]
}

Property Testing Functions

Core functions for running property-based tests.

/**
 * Check property with sufficient number of samples
 * @param rv - Generator producing test values
 * @param test - Property test function
 * @return ZIO effect producing TestResult
 */
def check[R, A, In](rv: Gen[R, A])(test: A => In)(implicit
  checkConstructor: CheckConstructor[R, In],
  sourceLocation: SourceLocation,
  trace: Trace
): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult]

/**
 * Check property for all values from finite generator
 * @param rv - Finite generator producing test values
 * @param test - Property test function  
 * @return ZIO effect producing TestResult
 */
def checkAll[R, A, In](rv: Gen[R, A])(test: A => In)(implicit
  checkConstructor: CheckConstructor[R, In],
  sourceLocation: SourceLocation,
  trace: Trace
): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult]

/**
 * Check property with specific number of samples
 * @param n - Number of samples to test
 * @return CheckN instance for applying to generators
 */
def checkN(n: Int): CheckVariants.CheckN

/**
 * Check property in parallel with sufficient samples
 * @param rv - Generator producing test values
 * @param parallelism - Number of parallel workers
 * @param test - Property test function
 * @return ZIO effect producing TestResult
 */
def checkPar[R, A, In](rv: Gen[R, A], parallelism: Int)(test: A => In)(implicit
  checkConstructor: CheckConstructor[R, In],
  sourceLocation: SourceLocation,
  trace: Trace
): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult]

/**
 * Check all values from finite generator in parallel
 * @param rv - Finite generator producing test values
 * @param parallelism - Number of parallel workers
 * @param test - Property test function
 * @return ZIO effect producing TestResult
 */
def checkAllPar[R, A, In](rv: Gen[R, A], parallelism: Int)(test: A => In)(implicit
  checkConstructor: CheckConstructor[R, In],
  sourceLocation: SourceLocation,
  trace: Trace
): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("property-based testing") {
  // Simple property test
  check(int) { n =>
    assertTrue((n + 1) > n)
  } &&
  
  // Test with multiple generators
  check(int, string) { (n, s) =>
    assertTrue(s.length >= 0 && n.toString.nonEmpty)
  } &&
  
  // Test with specific number of samples
  checkN(1000)(double) { d =>
    assertTrue(!d.isNaN || d.isNaN) // Always true
  } &&
  
  // Test all values from finite generator
  checkAll(oneOf(1, 2, 3, 4, 5)) { n =>
    assertTrue(n >= 1 && n <= 5)
  }
}

Primitive Generators

Generators for basic Scala types.

/** Boolean generator */
def boolean: Gen[Any, Boolean]

/** Byte generator with full range */
def byte: Gen[Any, Byte]

/** Byte generator within specified bounds */
def byteWith(min: Byte, max: Byte): Gen[Any, Byte]

/** Short generator with full range */
def short: Gen[Any, Short]

/** Short generator within specified bounds */
def shortWith(min: Short, max: Short): Gen[Any, Short]

/** Int generator with full range */
def int: Gen[Any, Int]

/** Int generator within specified bounds */
def intWith(min: Int, max: Int): Gen[Any, Int]

/** Long generator with full range */
def long: Gen[Any, Long]

/** Long generator within specified bounds */
def longWith(min: Long, max: Long): Gen[Any, Long]

/** Float generator */
def float: Gen[Any, Float]

/** Float generator within specified bounds */
def floatWith(min: Float, max: Float): Gen[Any, Float]

/** Double generator */
def double: Gen[Any, Double]

/** Double generator within specified bounds */
def doubleWith(min: Double, max: Double): Gen[Any, Double]

/** BigInt generator */
def bigInt: Gen[Any, BigInt]

/** BigInt generator within specified bounds */
def bigIntWith(min: BigInt, max: BigInt): Gen[Any, BigInt]

/** BigDecimal generator */
def bigDecimal: Gen[Any, BigDecimal]

/** BigDecimal generator within specified bounds */
def bigDecimalWith(min: BigDecimal, max: BigDecimal): Gen[Any, BigDecimal]

/** Java BigInteger generator */
def bigIntegerJava: Gen[Any, java.math.BigInteger]

/** Java BigDecimal generator */
def bigDecimalJava: Gen[Any, java.math.BigDecimal]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("primitive generators") {
  check(boolean) { b =>
    assertTrue(b == true || b == false)
  } &&
  
  check(intWith(1, 100)) { n =>
    assertTrue(n >= 1 && n <= 100)
  } &&
  
  check(doubleWith(0.0, 1.0)) { d =>
    assertTrue(d >= 0.0 && d <= 1.0)
  }
}

Character and String Generators

Generators for characters and strings with various constraints.

/** Character generator with full Unicode range */
def char: Gen[Any, Char]

/** Character generator within specified bounds */
def charWith(min: Char, max: Char): Gen[Any, Char]

/** Alphabetic character generator (a-z, A-Z) */
def alphaChar: Gen[Any, Char]

/** Alphanumeric character generator (a-z, A-Z, 0-9) */
def alphaNumericChar: Gen[Any, Char]

/** Numeric character generator (0-9) */
def numericChar: Gen[Any, Char]

/** ASCII character generator (0-127) */
def asciiChar: Gen[Any, Char]

/** Printable ASCII character generator */
def printableChar: Gen[Any, Char]

/** Hexadecimal character generator (0-9, a-f, A-F) */
def hexChar: Gen[Any, Char]

/** Lowercase hexadecimal character generator (0-9, a-f) */
def hexCharLower: Gen[Any, Char]

/** Uppercase hexadecimal character generator (0-9, A-F) */
def hexCharUpper: Gen[Any, Char]

/** Unicode character generator */
def unicodeChar: Gen[Any, Char]

/** Whitespace character generator */
def whitespaceChars: Gen[Any, Char]

/** String generator using printable characters */
def string: Gen[Any, String]

/** Non-empty string generator */
def string1: Gen[Any, String]

/** Fixed-length string generator */
def stringN(n: Int): Gen[Any, String]

/** Bounded-length string generator */
def stringBounded(min: Int, max: Int): Gen[Any, String]

/** Alphanumeric string generator */
def alphaNumericString: Gen[Any, String]

/** Bounded alphanumeric string generator */
def alphaNumericStringBounded(min: Int, max: Int): Gen[Any, String]

/** ASCII string generator */
def asciiString: Gen[Any, String]

/** ISO-8859-1 string generator */
def iso_8859_1: Gen[Any, String]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("character and string generators") {
  check(alphaChar) { c =>
    assertTrue(c.isLetter)
  } &&
  
  check(alphaNumericString) { s =>
    assertTrue(s.forall(c => c.isLetterOrDigit))
  } &&
  
  check(stringBounded(5, 10)) { s =>
    assertTrue(s.length >= 5 && s.length <= 10)
  }
}

Collection Generators

Generators for various collection types.

/** List generator using another generator */
def listOf[A](gen: Gen[Any, A]): Gen[Any, List[A]]

/** Non-empty list generator */
def listOf1[A](gen: Gen[Any, A]): Gen[Any, List[A]]

/** Fixed-size list generator */
def listOfN[A](n: Int)(gen: Gen[Any, A]): Gen[Any, List[A]]

/** Bounded-size list generator */
def listOfBounded[A](min: Int, max: Int)(gen: Gen[Any, A]): Gen[Any, List[A]]

/** Vector generator using another generator */
def vectorOf[A](gen: Gen[Any, A]): Gen[Any, Vector[A]]

/** Non-empty vector generator */
def vectorOf1[A](gen: Gen[Any, A]): Gen[Any, Vector[A]]

/** Fixed-size vector generator */
def vectorOfN[A](n: Int)(gen: Gen[Any, A]): Gen[Any, Vector[A]]

/** Bounded-size vector generator */
def vectorOfBounded[A](min: Int, max: Int)(gen: Gen[Any, A]): Gen[Any, Vector[A]]

/** Chunk generator using another generator */
def chunkOf[A](gen: Gen[Any, A]): Gen[Any, Chunk[A]]

/** Non-empty chunk generator */
def chunkOf1[A](gen: Gen[Any, A]): Gen[Any, Chunk[A]]

/** Fixed-size chunk generator */
def chunkOfN[A](n: Int)(gen: Gen[Any, A]): Gen[Any, Chunk[A]]

/** Bounded-size chunk generator */
def chunkOfBounded[A](min: Int, max: Int)(gen: Gen[Any, A]): Gen[Any, Chunk[A]]

/** Set generator using another generator */
def setOf[A](gen: Gen[Any, A]): Gen[Any, Set[A]]

/** Non-empty set generator */
def setOf1[A](gen: Gen[Any, A]): Gen[Any, Set[A]]

/** Fixed-size set generator */
def setOfN[A](n: Int)(gen: Gen[Any, A]): Gen[Any, Set[A]]

/** Bounded-size set generator */
def setOfBounded[A](min: Int, max: Int)(gen: Gen[Any, A]): Gen[Any, Set[A]]

/** Map generator using key and value generators */
def mapOf[K, V](keyGen: Gen[Any, K], valueGen: Gen[Any, V]): Gen[Any, Map[K, V]]

/** Non-empty map generator */
def mapOf1[K, V](keyGen: Gen[Any, K], valueGen: Gen[Any, V]): Gen[Any, Map[K, V]]

/** Fixed-size map generator */
def mapOfN[K, V](n: Int)(keyGen: Gen[Any, K], valueGen: Gen[Any, V]): Gen[Any, Map[K, V]]

/** Bounded-size map generator */
def mapOfBounded[K, V](min: Int, max: Int)(keyGen: Gen[Any, K], valueGen: Gen[Any, V]): Gen[Any, Map[K, V]]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("collection generators") {
  check(listOf(int)) { numbers =>
    assertTrue(numbers.forall(_.isInstanceOf[Int]))
  } &&
  
  check(listOfBounded(1, 5)(string)) { strings =>
    assertTrue(strings.size >= 1 && strings.size <= 5)
  } &&
  
  check(setOf(intWith(1, 10))) { numbers =>
    assertTrue(numbers.forall(n => n >= 1 && n <= 10))
  } &&
  
  check(mapOf(string, int)) { map =>
    assertTrue(map.keys.forall(_.isInstanceOf[String]) && 
               map.values.forall(_.isInstanceOf[Int]))
  }
}

Option and Either Generators

Generators for Option and Either types.

/** Option generator that may produce Some or None */
def option[A](gen: Gen[Any, A]): Gen[Any, Option[A]]

/** Some generator that always produces Some */
def some[A](gen: Gen[Any, A]): Gen[Any, Some[A]]

/** None generator that always produces None */
def none: Gen[Any, None.type]

/** Either generator using left and right generators */
def either[A, B](leftGen: Gen[Any, A], rightGen: Gen[Any, B]): Gen[Any, Either[A, B]]

Special Utility Generators

Generators for specific use cases and combinations.

/** Constant value generator */
def const[A](value: A): Gen[Any, A]

/** Constant sample generator with shrinking */
def constSample[R, A](sample: Sample[R, A]): Gen[R, A]

/** Generator from specific elements */
def elements[A](as: A*): Gen[Any, A]

/** Generator choosing from multiple generators */
def oneOf[A](gens: Gen[Any, A]*): Gen[Any, A]

/** Weighted generator selection */
def weighted[A](weightedGens: (Gen[Any, A], Double)*): Gen[Any, A]

/** Generator from iterable collection */
def fromIterable[A](as: Iterable[A]): Gen[Any, A]

/** Generator from ZIO effect */
def fromZIO[R, A](zio: ZIO[R, Nothing, A]): Gen[R, A]

/** Generator from ZIO sample effect */
def fromZIOSample[R, A](zio: ZIO[R, Nothing, Sample[R, A]]): Gen[R, A]

/** Lazy generator evaluation */
def suspend[R, A](gen: => Gen[R, A]): Gen[R, A]

/** Empty generator (produces no values) */
def empty[A]: Gen[Any, A]

/** Unit generator */
def unit: Gen[Any, Unit]

/** UUID generator */
def uuid: Gen[Any, java.util.UUID]

/** Currency generator */
def currency: Gen[Any, java.util.Currency]

/** Throwable generator */
def throwable: Gen[Any, Throwable]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("special generators") {
  check(const(42)) { n =>
    assertTrue(n == 42)
  } &&
  
  check(elements("red", "green", "blue")) { color =>
    assertTrue(Set("red", "green", "blue").contains(color))
  } &&
  
  check(oneOf(int, string.map(_.length))) { value =>
    assertTrue(value.isInstanceOf[Int])
  } &&
  
  check(option(string)) { maybeString =>
    assertTrue(maybeString.isEmpty || maybeString.get.isInstanceOf[String])
  }
}

Size-Related Generators

Generators that work with size parameters.

/** Current size generator */
def size: Gen[Sized, Int]

/** Size-dependent generator */
def sized[R, A](f: Int => Gen[R, A]): Gen[R with Sized, A]

/** Small-sized generator (size / 4) */
def small[R, A](gen: Gen[R, A]): Gen[R, A]

/** Medium-sized generator (size / 2) */
def medium[R, A](gen: Gen[R, A]): Gen[R, A]

/** Large-sized generator (size * 2) */
def large[R, A](gen: Gen[R, A]): Gen[R, A]

Mathematical Distribution Generators

Generators for specific mathematical distributions.

/** Uniform distribution generator */
def uniform[A: Numeric](min: A, max: A): Gen[Any, A]

/** Exponential distribution generator */
def exponential[A: Numeric](lambda: A): Gen[Any, A]

Combinator Functions

Functions for combining and transforming generators.

/** Collect all generator values into a collection */
def collectAll[R, A](gens: Iterable[Gen[R, A]]): Gen[R, List[A]]

/** Concatenate multiple generators */
def concatAll[R, A](gens: Iterable[Gen[R, A]]): Gen[R, A]

Usage Examples:

import zio.test._
import zio.test.Gen._

test("size and distribution generators") {
  check(sized(n => listOfN(n)(int))) { numbers =>
    assertTrue(numbers.size >= 0)
  } &&
  
  check(uniform(1.0, 10.0)) { d =>
    assertTrue(d >= 1.0 && d <= 10.0)
  } &&
  
  check(small(listOf(int))) { numbers =>
    assertTrue(numbers.size <= 25) // Typically smaller due to size reduction
  }
}