Advanced property-based testing with sophisticated generators, automatic shrinking, and configurable test execution.
Run property-based tests with generated inputs to verify properties hold across many test cases.
/**
* Check that a property holds for generated values from a generator
* @param rv generator for test values
* @param test property to verify for each generated value
* @return test result indicating success or failure with shrunk counterexamples
*/
def check[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]
/**
* Check property with two generators
*/
def check[R <: ZAny, A, B](rv1: Gen[R, A], rv2: Gen[R, B])(
test: (A, B) => TestResult
): ZIO[R with TestConfig, Nothing, TestResult]
/**
* Check property with up to 8 generators (additional overloads available)
*/
def check[R <: ZAny, A, B, C](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(
test: (A, B, C) => TestResult
): ZIO[R with TestConfig, Nothing, TestResult]
/**
* Check that property holds for ALL values from a finite generator
* @param rv finite, deterministic generator
* @param test property to verify
* @return test result
*/
def checkAll[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]
/**
* Check property in parallel for better performance with finite generators
* @param rv generator for test values
* @param parallelism number of parallel executions
* @param test property to verify
*/
def checkAllPar[R <: ZAny, A](rv: Gen[R, A], parallelism: Int)(
test: A => TestResult
): ZIO[R with TestConfig, Nothing, TestResult]Usage Examples:
import zio.test._
import zio.test.Gen._
// Basic property testing
test("list reverse is idempotent") {
check(listOf(anyInt)) { list =>
assertTrue(list.reverse.reverse == list)
}
}
// Multiple generators
test("addition is commutative") {
check(anyInt, anyInt) { (a, b) =>
assertTrue(a + b == b + a)
}
}
// Effectful property testing
test("database round trip") {
check(Gen.user) { user =>
for {
saved <- Database.save(user)
retrieved <- Database.findById(saved.id)
} yield assertTrue(retrieved.contains(user))
}
}
// Exhaustive testing with finite generator
test("boolean operations") {
checkAll(Gen.boolean, Gen.boolean) { (a, b) =>
assertTrue((a && b) == !((!a) || (!b))) // De Morgan's law
}
}Core generator trait for producing random values with shrinking capability.
/**
* Generator for values of type A in environment R
*/
trait Gen[+R, +A] {
/**
* Transform generated values
* @param f transformation function
* @return generator producing transformed values
*/
def map[B](f: A => B): Gen[R, B]
/**
* Chain generators together
* @param f function producing dependent generator
* @return generator with chained dependencies
*/
def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]
/**
* Filter generated values
* @param f predicate function
* @return generator producing only values satisfying predicate
*/
def filter(f: A => Boolean): Gen[R, A]
/**
* Generate a stream of samples with shrinking information
* @return stream of generated samples
*/
def sample: ZStream[R, Nothing, Sample[R, A]]
/**
* Combine with another generator using applicative
* @param that other generator
* @return generator producing tuples
*/
def <*>[R1 <: R, B](that: Gen[R1, B]): Gen[R1, (A, B)]
/**
* Generate values within specified size bounds
* @param min minimum size
* @param max maximum size
* @return sized generator
*/
def between(min: Int, max: Int): Gen[R with Sized, A]
}Built-in generators for basic data types.
// Numeric generators
val anyByte: Gen[Any, Byte]
val anyShort: Gen[Any, Short]
val anyInt: Gen[Any, Int]
val anyLong: Gen[Any, Long]
val anyFloat: Gen[Any, Float]
val anyDouble: Gen[Any, Double]
// Bounded numeric generators
def byte(min: Byte, max: Byte): Gen[Any, Byte]
def short(min: Short, max: Short): Gen[Any, Short]
def int(min: Int, max: Int): Gen[Any, Int]
def long(min: Long, max: Long): Gen[Any, Long]
def double(min: Double, max: Double): Gen[Any, Double]
// Character and string generators
val anyChar: Gen[Any, Char]
val anyString: Gen[Sized, String]
val anyASCIIString: Gen[Sized, String]
val alphaNumericString: Gen[Sized, String]
val alphaChar: Gen[Any, Char]
val numericChar: Gen[Any, Char]
// Boolean generator
val boolean: Gen[Any, Boolean]
// UUID generator
val anyUUID: Gen[Any, java.util.UUID]Usage Examples:
import zio.test.Gen._
// Basic generators
test("numeric properties") {
check(anyInt) { n =>
assertTrue(n + 0 == n)
}
}
// Bounded generators
test("percentage calculations") {
check(double(0.0, 100.0)) { percentage =>
assertTrue(percentage >= 0.0 && percentage <= 100.0)
}
}
// String generators
test("string length properties") {
check(anyString) { str =>
assertTrue(str.length >= 0 && str.reverse.length == str.length)
}
}Generators for collections with configurable sizes.
/**
* Generate lists of specified size
* @param n exact size of generated lists
* @param g generator for list elements
* @return generator producing lists of size n
*/
def listOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, List[A]]
/**
* Generate lists with sizes bounded by current Sized environment
* @param g generator for list elements
* @return generator producing variable-sized lists
*/
def listOf[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]
/**
* Generate lists with size between min and max
* @param min minimum list size
* @param max maximum list size
* @param g generator for list elements
*/
def listOfBounded[R, A](min: Int, max: Int)(g: Gen[R, A]): Gen[R, List[A]]
// Vector generators
def vectorOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Vector[A]]
def vectorOf[R, A](g: Gen[R, A]): Gen[R with Sized, Vector[A]]
// Set generators (automatically deduplicates)
def setOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Set[A]]
def setOf[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]
// Map generators
def mapOfN[R, A, B](n: Int)(g: Gen[R, (A, B)]): Gen[R, Map[A, B]]
def mapOf[R, A, B](g: Gen[R, (A, B)]): Gen[R with Sized, Map[A, B]]
// Non-empty collections
def listOf1[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]
def setOf1[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]Usage Examples:
import zio.test.Gen._
// Fixed-size collections
test("list operations") {
check(listOfN(5)(anyInt)) { list =>
assertTrue(list.size == 5 && list.reverse.reverse == list)
}
}
// Variable-size collections
test("set properties") {
check(setOf(anyInt)) { set =>
assertTrue(set.union(set) == set && set.intersect(set) == set)
}
}
// Non-empty collections
test("head and tail operations") {
check(listOf1(anyString)) { list =>
assertTrue(list.nonEmpty && list.head :: list.tail == list)
}
}
// Maps
test("map properties") {
check(mapOf(anyString.zip(anyInt))) { map =>
assertTrue(map.keys.toSet.size <= map.size)
}
}Functions for combining and transforming generators.
/**
* Choose randomly from provided generators
* @param first first generator option
* @param rest additional generator options
* @return generator that randomly selects from provided options
*/
def oneOf[R, A](first: Gen[R, A], rest: Gen[R, A]*): Gen[R, A]
/**
* Create generator from effectful computation
* @param effect ZIO effect producing values
* @return generator that runs the effect
*/
def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]
/**
* Generate values using unfold pattern
* @param s initial state
* @param f function from state to next state and value
* @return generator using unfold pattern
*/
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]
/**
* Choose from generators with specified weights
* @param generators weighted generator options
* @return generator that selects based on weights
*/
def weighted[R, A](generators: (Gen[R, A], Double)*): Gen[R, A]
/**
* Generate constant value
* @param a constant value to generate
* @return generator always producing the constant
*/
def const[A](a: => A): Gen[Any, A]
/**
* Generate from explicit list of values
* @param first first value option
* @param rest additional value options
* @return generator randomly selecting from provided values
*/
def elements[A](first: A, rest: A*): Gen[Any, A]
/**
* Generate optional values (Some or None)
* @param g generator for Some values
* @return generator producing Option[A]
*/
def option[R, A](g: Gen[R, A]): Gen[R, Option[A]]
/**
* Generate Either values
* @param left generator for Left values
* @param right generator for Right values
* @return generator producing Either[A, B]
*/
def either[R, A, B](left: Gen[R, A], right: Gen[R, B]): Gen[R, Either[A, B]]
/**
* Generate tuples
* @param g1 generator for first element
* @param g2 generator for second element
* @return generator producing tuples
*/
def zip[R, A, B](g1: Gen[R, A], g2: Gen[R, B]): Gen[R, (A, B)]Usage Examples:
import zio.test.Gen._
// Choose from generators
val stringOrInt: Gen[Any, Any] = oneOf(anyString, anyInt)
// Weighted choice
val mostlyPositive: Gen[Any, Int] = weighted(
int(1, 100) -> 0.8,
int(-100, 0) -> 0.2
)
// Constant and elements
val httpMethods = elements("GET", "POST", "PUT", "DELETE")
val alwaysTrue = const(true)
// Option and Either
test("optional values") {
check(option(anyInt)) { optInt =>
optInt match {
case Some(n) => assertTrue(n.toString.nonEmpty)
case None => assertTrue(true)
}
}
}
test("either values") {
check(either(anyString, anyInt)) { stringOrInt =>
stringOrInt match {
case Left(s) => assertTrue(s.isInstanceOf[String])
case Right(n) => assertTrue(n.isInstanceOf[Int])
}
}
}Building domain-specific generators for complex data types.
/**
* Create generator that may fail to produce values
* @param pf partial function defining generation logic
* @return generator with filtering logic
*/
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]
/**
* Create recursive generators with size control
* @param base base case generator for small sizes
* @param rec recursive case for larger sizes
* @return size-aware recursive generator
*/
def sized[R, A](f: Int => Gen[R, A]): Gen[R with Sized, A]
/**
* Generate from a ZIO effect
* @param effect effectful value generation
* @return generator wrapping the effect
*/
def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]Usage Examples:
import zio.test.Gen._
// Custom data types
case class User(id: Int, name: String, email: String, age: Int)
val genUser: Gen[Any, User] = for {
id <- int(1, 10000)
name <- elements("Alice", "Bob", "Charlie", "Diana")
domain <- elements("example.com", "test.org", "demo.net")
age <- int(18, 80)
} yield User(id, name, s"${name.toLowerCase}@$domain", age)
// Recursive data structures
sealed trait Tree[A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
def genTree[A](genA: Gen[Any, A]): Gen[Sized, Tree[A]] = {
val genLeaf = genA.map(Leaf(_))
val genBranch = for {
left <- genTree(genA)
right <- genTree(genA)
} yield Branch(left, right)
sized { size =>
if (size <= 1) genLeaf
else oneOf(genLeaf, genBranch)
}
}
// Using custom generators
test("user validation") {
check(genUser) { user =>
assertTrue(
user.id > 0 &&
user.name.nonEmpty &&
user.email.contains("@") &&
user.age >= 18
)
}
}Represents generated values with shrinking information for counterexample minimization.
/**
* A generated sample with shrinking capability
*/
trait Sample[+R, +A] {
/**
* The generated value
*/
def value: A
/**
* Stream of shrunk variants of this sample
* @return stream of smaller samples for counterexample minimization
*/
def shrink: ZStream[R, Nothing, Sample[R, A]]
/**
* Transform the sample value
* @param f transformation function
* @return sample with transformed value
*/
def map[B](f: A => B): Sample[R, B]
/**
* Transform with environment modification
* @param f environment transformation
* @return sample in transformed environment
*/
def mapZIO[R1, B](f: A => ZIO[R1, Nothing, B]): Sample[R1, B]
}Controls the size of generated collections and data structures.
/**
* Environment service that provides size bounds for generators
*/
case class Sized(size: Int) extends AnyVal
object Sized {
/**
* Create a Sized service with fixed size
* @param size the size value
* @return layer providing Sized service
*/
def live(size: Int): ULayer[Sized]
/**
* Access current size from environment
*/
val size: URIO[Sized, Int]
}Usage Examples:
import zio.test._
// Control generation size
test("large collections").provideLayer(Sized.live(1000)) {
check(listOf(anyInt)) { largeList =>
assertTrue(largeList.size <= 1000)
}
}
// Size-aware generators
val genSizedString: Gen[Sized, String] = sized { size =>
listOfN(size)(alphaChar).map(_.mkString)
}