or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application.mdconcurrency.mdcore-effects.mddependency-injection.mderror-handling.mdindex.mdmetrics.mdresource-management.mdservices.mdstm.mdstreams.mdtesting.md
tile.json

testing.mddocs/

ZIO Test Framework

ZIO Test provides a comprehensive testing framework with property-based testing, test services, and seamless integration with the ZIO effect system for testing concurrent and async code.

Capabilities

Test Specifications

Define test suites and individual test cases with full ZIO effect support and dependency injection.

/**
 * A test specification defines a suite of tests
 */
sealed trait Spec[-R, +E] {
  /** Transform the environment type */
  def provideLayer[E1 >: E, R0](layer: ZLayer[R0, E1, R]): Spec[R0, E1]
  
  /** Add test aspects to modify test behavior */
  def @@[R1 <: R](aspect: TestAspect[R1, E]): Spec[R1, E]
  
  /** Map over the error type */
  def mapError[E2](f: E => E2): Spec[R, E2]
}

/**
 * Base class for ZIO test suites
 */
abstract class ZIOSpec[R] extends ZIOApp {
  /** Define the test specification */
  def spec: Spec[R, Any]
  
  /** Provide dependencies for tests */
  def bootstrap: ZLayer[Any, Any, R] = ZLayer.empty
}

/**
 * Default test suite with no special requirements
 */
abstract class ZIOSpecDefault extends ZIOSpec[Any] {
  final def bootstrap: ZLayer[Any, Any, Any] = ZLayer.empty
}

/**
 * Test construction methods
 */
object ZIOSpecDefault {
  /** Create a simple test */
  def test(label: String)(assertion: => TestResult): Spec[Any, Nothing]
  
  /** Create a test with full ZIO effects */
  def testM[R, E](label: String)(assertion: ZIO[R, E, TestResult]): Spec[R, E]
  
  /** Create a test suite */
  def suite(label: String)(specs: Spec[Any, Any]*): Spec[Any, Any]
  
  /** Create a test suite with environment requirements */
  def suiteM[R, E](label: String)(specs: ZIO[R, E, Spec[R, E]]): Spec[R, E]
}

Usage Examples:

import zio._
import zio.test._

object MyServiceSpec extends ZIOSpecDefault {
  def spec = suite("MyService")(
    test("should handle simple operations") {
      val result = 2 + 2
      assertTrue(result == 4)
    },
    
    test("should work with ZIO effects") {
      for {
        result <- ZIO.succeed(42)
      } yield assertTrue(result > 0)
    },
    
    suite("error handling")(
      test("should catch failures") {
        val failing = ZIO.fail("error")
        assertZIO(failing.flip)(equalTo("error"))
      }
    )
  )
}

// With dependency injection
object DatabaseSpec extends ZIOSpec[Database] {
  def spec = suite("Database")(
    test("should save and retrieve users") {
      for {
        db   <- ZIO.service[Database]
        user <- db.save(User("alice", "alice@example.com"))
        retrieved <- db.findById(user.id)
      } yield assertTrue(retrieved.contains(user))
    }
  )
  
  def bootstrap = DatabaseLive.layer
}

Assertions

Comprehensive assertion library for testing values, effects, and complex conditions.

/**
 * Represents the result of a single test
 */
sealed trait TestResult {
  /** Combine with another test result using logical AND */
  def &&(that: => TestResult): TestResult
  
  /** Combine with another test result using logical OR */
  def ||(that: => TestResult): TestResult
  
  /** Negate the test result */
  def unary_! : TestResult
  
  /** Add a custom label to the result */
  def label(label: String): TestResult
}

/**
 * Type-safe assertions for testing
 */
sealed trait Assertion[-A] {
  /** Test the assertion against a value */
  def test(a: A): TestResult
  
  /** Combine with another assertion using logical AND */
  def &&[A1 <: A](that: Assertion[A1]): Assertion[A1]
  
  /** Combine with another assertion using logical OR */
  def ||[A1 <: A](that: Assertion[A1]): Assertion[A1]
  
  /** Negate the assertion */
  def unary_! : Assertion[A]
}

/**
 * Common assertion constructors
 */
object Assertion {
  /** Assert equality */
  def equalTo[A](expected: A): Assertion[A]
  
  /** Assert approximate equality for numbers */
  def approximatelyEquals(expected: Double, tolerance: Double): Assertion[Double]
  
  /** Assert value is true */
  val isTrue: Assertion[Boolean]
  
  /** Assert value is false */
  val isFalse: Assertion[Boolean]
  
  /** Assert value is empty (for collections) */
  val isEmpty: Assertion[Iterable[Any]]
  
  /** Assert value is non-empty */
  val isNonEmpty: Assertion[Iterable[Any]]
  
  /** Assert collection contains element */
  def contains[A](element: A): Assertion[Iterable[A]]
  
  /** Assert string contains substring */
  def containsString(substring: String): Assertion[String]
  
  /** Assert string starts with prefix */
  def startsWith(prefix: String): Assertion[String]
  
  /** Assert string ends with suffix */
  def endsWith(suffix: String): Assertion[String]
  
  /** Assert string matches regex */
  def matchesRegex(regex: String): Assertion[String]
  
  /** Assert value is greater than */
  def isGreaterThan[A](expected: A)(implicit ord: Ordering[A]): Assertion[A]
  
  /** Assert value is less than */
  def isLessThan[A](expected: A)(implicit ord: Ordering[A]): Assertion[A]
  
  /** Assert value is within range */
  def isWithin[A](min: A, max: A)(implicit ord: Ordering[A]): Assertion[A]
  
  /** Assert all elements satisfy condition */
  def forall[A](assertion: Assertion[A]): Assertion[Iterable[A]]
  
  /** Assert at least one element satisfies condition */
  def exists[A](assertion: Assertion[A]): Assertion[Iterable[A]]
  
  /** Assert collection has specific size */
  def hasSize[A](expected: Int): Assertion[Iterable[A]]
}

/**
 * Convenient assertion methods
 */
def assertTrue(condition: => Boolean): TestResult
def assert[A](value: A)(assertion: Assertion[A]): TestResult
def assertZIO[R, E, A](effect: ZIO[R, E, A])(assertion: Assertion[A]): ZIO[R, E, TestResult]

Usage Examples:

// Basic assertions
test("basic assertions") {
  assertTrue(2 + 2 == 4) &&
  assert("hello world")(containsString("world")) &&
  assert(List(1, 2, 3))(hasSize(3) && contains(2))
}

// Effect assertions
test("effect assertions") {
  val computation = ZIO.succeed(42)
  assertZIO(computation)(isGreaterThan(40))
}

// Complex conditions
test("complex conditions") {
  val data = List("apple", "banana", "cherry")
  assert(data)(
    hasSize(3) &&
    forall(startsWith("a") || startsWith("b") || startsWith("c")) &&
    exists(containsString("nan"))
  )
}

// Custom labels
test("with custom labels") {
  val result = complexCalculation()
  assert(result)(isGreaterThan(0)).label("result should be positive") &&
  assert(result)(isLessThan(100)).label("result should be reasonable")
}

Property-Based Testing

Generate random test data and verify properties hold across many test cases.

/**
 * Generator for producing random test data
 */
sealed trait Gen[+R, +A] {
  /** Transform generated values */
  def map[B](f: A => B): Gen[R, B]
  
  /** Chain generators together */
  def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]
  
  /** Filter generated values */
  def filter(f: A => Boolean): Gen[R, A]
  
  /** Generate optional values */
  def optional: Gen[R, Option[A]]
  
  /** Generate lists of values */
  def list: Gen[R, List[A]]
  
  /** Generate lists of specific size */
  def listOfN(n: Int): Gen[R, List[A]]
  
  /** Generate values between bounds */
  def bounded(min: A, max: A)(implicit ord: Ordering[A]): Gen[R, A]
}

/**
 * Common generators
 */
object Gen {
  /** Generate random integers */
  val int: Gen[Any, Int]
  
  /** Generate integers in range */
  def int(min: Int, max: Int): Gen[Any, Int]
  
  /** Generate random strings */
  val string: Gen[Any, String]
  
  /** Generate alphanumeric strings */
  val alphaNumericString: Gen[Any, String]
  
  /** Generate strings of specific length */
  def stringN(n: Int): Gen[Any, String]
  
  /** Generate random booleans */
  val boolean: Gen[Any, Boolean]
  
  /** Generate random doubles */
  val double: Gen[Any, Double]
  
  /** Generate from a list of options */
  def oneOf[A](as: A*): Gen[Any, A]
  
  /** Generate from weighted options */
  def weighted[A](weightedValues: (A, Double)*): Gen[Any, A]
  
  /** Generate constant values */
  def const[A](a: A): Gen[Any, A]
  
  /** Generate collections */
  def listOf[R, A](gen: Gen[R, A]): Gen[R, List[A]]
  def setOf[R, A](gen: Gen[R, A]): Gen[R, Set[A]]
  def mapOf[R, K, V](keyGen: Gen[R, K], valueGen: Gen[R, V]): Gen[R, Map[K, V]]
  
  /** Generate case classes */
  def zip[R, A, B](genA: Gen[R, A], genB: Gen[R, B]): Gen[R, (A, B)]
  def zip3[R, A, B, C](genA: Gen[R, A], genB: Gen[R, B], genC: Gen[R, C]): Gen[R, (A, B, C)]
}

/**
 * Property-based test construction
 */
def check[R, A](gen: Gen[R, A])(test: A => TestResult): ZIO[R, Nothing, TestResult]
def checkAll[R, A](gen: Gen[R, A])(test: A => TestResult): ZIO[R, Nothing, TestResult]
def checkN(n: Int): CheckN

Usage Examples:

import zio.test.Gen._

// Simple property test
test("string reverse property") {
  check(string) { s =>
    assertTrue(s.reverse.reverse == s)
  }
}

// Multiple generators
test("addition is commutative") {
  check(int, int) { (a, b) =>
    assertTrue(a + b == b + a)
  }
}

// Custom generators
val positiveInt = int(1, 1000)
val email = for {
  name   <- alphaNumericString
  domain <- oneOf("gmail.com", "yahoo.com", "example.org")
} yield s"$name@$domain"

test("email validation") {
  check(email) { email =>
    assertTrue(email.contains("@") && email.contains("."))
  }
}

// Complex data structures
case class User(name: String, age: Int, email: String)

val userGen = for {
  name  <- alphaNumericString
  age   <- int(18, 100)
  email <- email
} yield User(name, age, email)

test("user serialization") {
  check(userGen) { user =>
    val serialized = serialize(user)
    val deserialized = deserialize(serialized)
    assertTrue(deserialized == user)
  }
}

Test Services

Mock implementations of ZIO services for deterministic testing without external dependencies.

/**
 * Test implementation of Clock service for deterministic time testing
 */
trait TestClock extends Clock {
  /** Adjust the test clock by a duration */
  def adjust(duration: Duration): UIO[Unit]
  
  /** Set the test clock to a specific instant */
  def setTime(instant: Instant): UIO[Unit]
  
  /** Get current test time */
  def instant: UIO[Instant]
  
  /** Get time zone */
  def timeZone: UIO[ZoneId]
}

/**
 * Test implementation of Console for capturing output
 */
trait TestConsole extends Console {
  /** Get all output written to stdout */
  def output: UIO[Vector[String]]
  
  /** Get all output written to stderr */
  def errorOutput: UIO[Vector[String]]
  
  /** Clear all captured output */
  def clearOutput: UIO[Unit]
  
  /** Feed input to be read by readLine */
  def feedLines(lines: String*): UIO[Unit]
}

/**
 * Test implementation of Random for predictable randomness
 */
trait TestRandom extends Random {
  /** Set the random seed */
  def setSeed(seed: Long): UIO[Unit]
  
  /** Feed specific random values */
  def feedInts(ints: Int*): UIO[Unit]
  def feedDoubles(doubles: Double*): UIO[Unit]
  def feedBooleans(booleans: Boolean*): UIO[Unit]
}

/**
 * Test implementation of System for mocking environment
 */
trait TestSystem extends System {
  /** Set environment variables */
  def putEnv(name: String, value: String): UIO[Unit]
  
  /** Set system properties */
  def putProperty(name: String, value: String): UIO[Unit]
  
  /** Clear environment variables */
  def clearEnv(name: String): UIO[Unit]
  
  /** Clear system properties */
  def clearProperty(name: String): UIO[Unit]
}

Usage Examples:

// Testing with TestClock
test("timeout behavior") {
  for {
    fiber  <- longRunningTask.timeout(5.seconds).fork
    _      <- TestClock.adjust(6.seconds)
    result <- fiber.join
  } yield assertTrue(result.isEmpty)
}

// Testing with TestConsole
test("console output") {
  for {
    _      <- Console.printLine("Hello")
    _      <- Console.printLine("World")
    output <- TestConsole.output
  } yield assertTrue(output == Vector("Hello", "World"))
}

// Testing with TestRandom
test("random behavior") {
  for {
    _      <- TestRandom.feedInts(1, 2, 3)
    first  <- Random.nextInt
    second <- Random.nextInt
    third  <- Random.nextInt
  } yield assertTrue(first == 1 && second == 2 && third == 3)
}

// Combined service testing
test("application behavior") {
  val program = for {
    config <- ZIO.service[AppConfig]
    _      <- Console.printLine(s"Starting on port ${config.port}")
    _      <- Clock.sleep(1.second)
    _      <- Console.printLine("Started successfully")
  } yield ()
  
  program.provide(
    TestConsole.layer,
    TestClock.layer,
    ZLayer.succeed(AppConfig(8080))
  ) *> 
  for {
    _      <- TestClock.adjust(1.second)
    output <- TestConsole.output
  } yield assertTrue(
    output.contains("Starting on port 8080") &&
    output.contains("Started successfully")
  )
}

Test Aspects

Modify test behavior with reusable aspects for timeouts, retries, parallelism, and more.

/**
 * Test aspects modify how tests are executed
 */
sealed trait TestAspect[-R, +E] {
  /** Combine with another aspect */
  def @@[R1 <: R](that: TestAspect[R1, E]): TestAspect[R1, E]
}

/**
 * Common test aspects
 */
object TestAspect {
  /** Set timeout for tests */
  def timeout(duration: Duration): TestAspect[Any, Nothing]
  
  /** Retry failed tests */
  def retry(n: Int): TestAspect[Any, Nothing]
  
  /** Run tests eventually (keep retrying until success) */
  val eventually: TestAspect[Any, Nothing]
  
  /** Run tests in parallel */
  val parallel: TestAspect[Any, Nothing]
  
  /** Run tests sequentially */
  val sequential: TestAspect[Any, Nothing]
  
  /** Ignore/skip tests */
  val ignore: TestAspect[Any, Nothing]
  
  /** Run only on specific platforms */
  def jvmOnly: TestAspect[Any, Nothing]
  def jsOnly: TestAspect[Any, Nothing]
  def nativeOnly: TestAspect[Any, Nothing]
  
  /** Repeat tests multiple times */
  def repeats(n: Int): TestAspect[Any, Nothing]
  
  /** Add samples for property-based tests */
  def samples(n: Int): TestAspect[Any, Nothing]
  
  /** Shrink failing test cases */
  val shrinks: TestAspect[Any, Nothing]
  
  /** Run with specific test data */
  def withLiveClock: TestAspect[Any, Nothing]
  def withLiveConsole: TestAspect[Any, Nothing]
  def withLiveRandom: TestAspect[Any, Nothing]
}

Usage Examples:

// Timeout aspect
test("long running operation") {
  heavyComputation
} @@ TestAspect.timeout(30.seconds)

// Retry flaky tests
test("flaky network operation") {
  networkCall
} @@ TestAspect.retry(3)

// Eventually succeeding tests
test("eventual consistency") {
  checkEventualConsistency
} @@ TestAspect.eventually

// Platform-specific tests
test("JVM-specific functionality") {
  jvmSpecificCode
} @@ TestAspect.jvmOnly

// Property test configuration
test("complex property") {
  check(complexGen)(complexProperty)
} @@ TestAspect.samples(1000) @@ TestAspect.shrinks

// Suite-level aspects
suite("integration tests")(
  test("database operations") { ... },
  test("api calls") { ... }
) @@ TestAspect.sequential @@ TestAspect.timeout(5.minutes)