ZIO Test is a zero-dependency testing library that makes it easy to test effectual programs
Deterministic test services that replace system services during testing for predictable, reproducible test execution.
Deterministic clock service for controlling time progression in tests.
/**
* Test clock that allows manual time control
*/
trait TestClock extends Clock {
/**
* Advance the clock by the specified duration
* @param duration amount to advance time
* @return effect that advances the clock
*/
def adjust(duration: Duration): UIO[Unit]
/**
* Set the clock to an absolute time
* @param duration absolute time from epoch
* @return effect that sets the clock time
*/
def setTime(duration: Duration): UIO[Unit]
/**
* Get the current time zone
* @return current time zone
*/
def timeZone: UIO[ZoneId]
/**
* Set the time zone for the clock
* @param zone new time zone
* @return effect that sets the time zone
*/
def setTimeZone(zone: ZoneId): UIO[Unit]
/**
* Sleep for the specified duration (uses test time)
* @param duration duration to sleep
* @return effect that completes after the duration
*/
override def sleep(duration: Duration): UIO[Unit]
/**
* Get current time in specified time unit
* @param unit time unit for the result
* @return current time in the specified unit
*/
override def currentTime(unit: TimeUnit): UIO[Long]
/**
* Get current time as instant
* @return current time instant
*/
override def instant: UIO[Instant]
}
object TestClock {
/**
* Default test clock implementation starting at epoch
*/
val default: ULayer[TestClock]
/**
* Create test clock starting at specified time
* @param startTime initial clock time
* @return layer providing test clock
*/
def live(startTime: Instant): ULayer[TestClock]
}Usage Examples:
import zio.test._
import zio._
import java.time.Instant
import java.util.concurrent.TimeUnit
// Basic time manipulation
test("time-dependent operation") {
for {
clock <- ZIO.service[TestClock]
start <- clock.currentTime(TimeUnit.MILLISECONDS)
_ <- clock.adjust(1.hour)
end <- clock.currentTime(TimeUnit.MILLISECONDS)
} yield assertTrue(end - start == 3600000)
}
// Testing scheduled operations
test("scheduled task execution") {
for {
clock <- ZIO.service[TestClock]
ref <- Ref.make(0)
// Schedule task to run every minute
fiber <- (ZIO.sleep(1.minute) *> ref.update(_ + 1)).forever.fork
// Advance time and check execution
_ <- clock.adjust(5.minutes)
count <- ref.get
_ <- fiber.interrupt
} yield assertTrue(count == 5)
}
// Testing timeout behavior
test("operation timeout") {
for {
clock <- ZIO.service[TestClock]
fiber <- (ZIO.sleep(10.seconds) *> ZIO.succeed("completed")).fork
_ <- clock.adjust(5.seconds)
result <- fiber.poll
} yield assertTrue(result.isEmpty) // Should still be running
}Console service for testing input/output operations with configurable responses.
/**
* Test console that captures output and provides controllable input
*/
trait TestConsole extends Console {
/**
* Get all captured output lines
* @return vector of output lines
*/
def output: UIO[Vector[String]]
/**
* Clear all captured output
* @return effect that clears output buffer
*/
def clearOutput: UIO[Unit]
/**
* Provide input lines for console reading
* @param lines input lines to feed to console
* @return effect that feeds the lines
*/
def feedLines(lines: String*): UIO[Unit]
/**
* Get all captured error output
* @return vector of error output lines
*/
def errorOutput: UIO[Vector[String]]
/**
* Clear all captured error output
* @return effect that clears error output buffer
*/
def clearErrorOutput: UIO[Unit]
/**
* Print line to output (captured)
* @param line line to print
* @return effect that prints the line
*/
override def printLine(line: Any): IO[IOException, Unit]
/**
* Print to output without newline (captured)
* @param text text to print
* @return effect that prints the text
*/
override def print(text: Any): IO[IOException, Unit]
/**
* Read line from input (uses fed lines)
* @return effect that reads a line
*/
override def readLine: IO[IOException, String]
}
object TestConsole {
/**
* Debug console that prints to real console and captures
*/
val debug: ULayer[TestConsole]
/**
* Silent console that only captures without real output
*/
val silent: ULayer[TestConsole]
}Usage Examples:
import zio.test._
import zio._
import java.io.IOException
// Capture and verify output
test("application output") {
for {
_ <- Console.printLine("Hello, World!")
_ <- Console.printLine("ZIO Test is awesome!")
console <- ZIO.service[TestConsole]
output <- console.output
} yield assertTrue(
output == Vector("Hello, World!", "ZIO Test is awesome!")
)
}
// Test interactive console programs
test("interactive program") {
def interactiveProgram: ZIO[Console, IOException, String] =
for {
_ <- Console.printLine("What's your name?")
name <- Console.readLine
_ <- Console.printLine(s"Hello, $name!")
} yield name
for {
console <- ZIO.service[TestConsole]
_ <- console.feedLines("Alice")
result <- interactiveProgram
output <- console.output
} yield assertTrue(
result == "Alice" &&
output == Vector("What's your name?", "Hello, Alice!")
)
}
// Test error output
test("error logging") {
def logError(message: String): ZIO[Console, IOException, Unit] =
Console.printLineError(s"ERROR: $message")
for {
_ <- logError("Something went wrong")
console <- ZIO.service[TestConsole]
errorOutput <- console.errorOutput
} yield assertTrue(errorOutput == Vector("ERROR: Something went wrong"))
}Deterministic random service for reproducible random number generation in tests.
/**
* Test random service with controllable seed and predefined values
*/
trait TestRandom extends Random {
/**
* Set the random seed for reproducible generation
* @param seed random seed value
* @return effect that sets the seed
*/
def setSeed(seed: Long): UIO[Unit]
/**
* Feed predefined boolean values
* @param booleans boolean values to return from nextBoolean
* @return effect that feeds the values
*/
def feedBooleans(booleans: Boolean*): UIO[Unit]
/**
* Clear all fed boolean values
* @return effect that clears boolean buffer
*/
def clearBooleans: UIO[Unit]
/**
* Feed predefined integer values
* @param ints integer values to return from nextInt
* @return effect that feeds the values
*/
def feedInts(ints: Int*): UIO[Unit]
/**
* Clear all fed integer values
* @return effect that clears integer buffer
*/
def clearInts: UIO[Unit]
/**
* Feed predefined long values
* @param longs long values to return from nextLong
* @return effect that feeds the values
*/
def feedLongs(longs: Long*): UIO[Unit]
/**
* Clear all fed long values
* @return effect that clears long buffer
*/
def clearLongs: UIO[Unit]
/**
* Feed predefined double values
* @param doubles double values to return from nextDouble
* @return effect that feeds the values
*/
def feedDoubles(doubles: Double*): UIO[Unit]
/**
* Clear all fed double values
* @return effect that clears double buffer
*/
def clearDoubles: UIO[Unit]
/**
* Generate next boolean (uses fed values or seed-based generation)
* @return effect producing boolean value
*/
override def nextBoolean: UIO[Boolean]
/**
* Generate next integer in range (uses fed values or seed-based generation)
* @param n upper bound (exclusive)
* @return effect producing integer value
*/
override def nextInt(n: Int): UIO[Int]
/**
* Generate next long (uses fed values or seed-based generation)
* @return effect producing long value
*/
override def nextLong: UIO[Long]
/**
* Generate next double (uses fed values or seed-based generation)
* @return effect producing double value
*/
override def nextDouble: UIO[Double]
}
object TestRandom {
/**
* Deterministic random with fixed seed
* @param seed initial seed value
* @return layer providing test random
*/
def deterministic(seed: Long): ULayer[TestRandom]
/**
* Deterministic random with default seed (0)
*/
val deterministic: ULayer[TestRandom]
}Usage Examples:
import zio.test._
import zio._
// Reproducible random testing
test("random behavior with fixed seed") {
def randomOperation: ZIO[Random, Nothing, List[Int]] =
ZIO.collectAll(List.fill(5)(Random.nextInt(100)))
for {
random <- ZIO.service[TestRandom]
_ <- random.setSeed(12345L)
result1 <- randomOperation
_ <- random.setSeed(12345L)
result2 <- randomOperation
} yield assertTrue(result1 == result2) // Same seed = same results
}
// Control random values explicitly
test("specific random sequence") {
def coinFlips(n: Int): ZIO[Random, Nothing, List[String]] =
ZIO.collectAll(
List.fill(n)(Random.nextBoolean.map(if (_) "heads" else "tails"))
)
for {
random <- ZIO.service[TestRandom]
_ <- random.feedBooleans(true, false, true, true, false)
flips <- coinFlips(5)
} yield assertTrue(
flips == List("heads", "tails", "heads", "heads", "tails")
)
}
// Test random distributions
test("random distribution properties") {
def generateSample: ZIO[Random, Nothing, List[Int]] =
ZIO.collectAll(List.fill(1000)(Random.nextInt(100)))
for {
random <- ZIO.service[TestRandom]
_ <- random.setSeed(42L)
sample <- generateSample
average = sample.sum.toDouble / sample.size
} yield assertTrue(average >= 40.0 && average <= 60.0) // Should be around 50
}System service for testing environment variables and system properties.
/**
* Test system service with controllable environment and properties
*/
trait TestSystem extends System {
/**
* Set environment variable for testing
* @param name variable name
* @param value variable value
* @return effect that sets the variable
*/
def putEnv(name: String, value: String): UIO[Unit]
/**
* Clear environment variable
* @param name variable name to clear
* @return effect that clears the variable
*/
def clearEnv(name: String): UIO[Unit]
/**
* Set system property for testing
* @param name property name
* @param value property value
* @return effect that sets the property
*/
def putProperty(name: String, value: String): UIO[Unit]
/**
* Clear system property
* @param name property name to clear
* @return effect that clears the property
*/
def clearProperty(name: String): UIO[Unit]
/**
* Get environment variable
* @param name variable name
* @return effect producing optional variable value
*/
override def env(name: String): IO[SecurityException, Option[String]]
/**
* Get system property
* @param name property name
* @return effect producing optional property value
*/
override def property(name: String): IO[Throwable, Option[String]]
/**
* Get line separator for current system
* @return line separator string
*/
override def lineSeparator: UIO[String]
}
object TestSystem {
/**
* Default test system implementation
*/
val default: ULayer[TestSystem]
/**
* Test system with initial environment and properties
* @param env initial environment variables
* @param props initial system properties
* @return layer providing test system
*/
def live(
env: Map[String, String] = Map.empty,
props: Map[String, String] = Map.empty
): ULayer[TestSystem]
}Usage Examples:
import zio.test._
import zio._
// Test environment-dependent behavior
test("application configuration from environment") {
def getConfig: ZIO[System, SecurityException, AppConfig] =
for {
host <- System.env("DB_HOST").map(_.getOrElse("localhost"))
port <- System.env("DB_PORT").map(_.getOrElse("5432"))
} yield AppConfig(host, port.toInt)
for {
system <- ZIO.service[TestSystem]
_ <- system.putEnv("DB_HOST", "test-db.example.com")
_ <- system.putEnv("DB_PORT", "3306")
config <- getConfig
} yield assertTrue(
config.host == "test-db.example.com" &&
config.port == 3306
)
}
// Test system property behavior
test("debug mode from system property") {
def isDebugMode: ZIO[System, Throwable, Boolean] =
System.property("debug").map(_.contains("true"))
for {
system <- ZIO.service[TestSystem]
_ <- system.putProperty("debug", "true")
debug1 <- isDebugMode
_ <- system.clearProperty("debug")
debug2 <- isDebugMode
} yield assertTrue(debug1 && !debug2)
}
// Test cross-platform line separator handling
test("text file generation") {
def generateTextFile(lines: List[String]): ZIO[System, Nothing, String] =
for {
separator <- System.lineSeparator
} yield lines.mkString(separator)
for {
result <- generateTextFile(List("line1", "line2", "line3"))
} yield assertTrue(result.contains("line1") && result.contains("line2"))
}
case class AppConfig(host: String, port: Int)Convenience functions for accessing test services from the test environment.
/**
* Access TestClock service
* @return effect producing TestClock instance
*/
def testClock: URIO[TestClock, TestClock]
/**
* Access TestClock and apply function
* @param f function to apply to TestClock
* @return effect with TestClock applied to function
*/
def testClockWith[R, E, A](f: TestClock => ZIO[R, E, A]): ZIO[R with TestClock, E, A]
/**
* Access TestConsole service
* @return effect producing TestConsole instance
*/
def testConsole: URIO[TestConsole, TestConsole]
/**
* Access TestConsole and apply function
* @param f function to apply to TestConsole
* @return effect with TestConsole applied to function
*/
def testConsoleWith[R, E, A](f: TestConsole => ZIO[R, E, A]): ZIO[R with TestConsole, E, A]
/**
* Access TestRandom service
* @return effect producing TestRandom instance
*/
def testRandom: URIO[TestRandom, TestRandom]
/**
* Access TestRandom and apply function
* @param f function to apply to TestRandom
* @return effect with TestRandom applied to function
*/
def testRandomWith[R, E, A](f: TestRandom => ZIO[R, E, A]): ZIO[R with TestRandom, E, A]
/**
* Access TestSystem service
* @return effect producing TestSystem instance
*/
def testSystem: URIO[TestSystem, TestSystem]
/**
* Access TestSystem and apply function
* @param f function to apply to TestSystem
* @return effect with TestSystem applied to function
*/
def testSystemWith[R, E, A](f: TestSystem => ZIO[R, E, A]): ZIO[R with TestSystem, E, A]Usage Examples:
import zio.test._
import zio._
// Direct service access
test("service access patterns") {
for {
clock <- testClock
_ <- clock.adjust(1.hour)
console <- testConsole
_ <- console.feedLines("test input")
random <- testRandom
_ <- random.setSeed(123L)
system <- testSystem
_ <- system.putEnv("TEST_VAR", "test_value")
} yield assertTrue(true)
}
// Service access with functions
test("service access with functions") {
for {
_ <- testClockWith(_.adjust(2.hours))
output <- testConsoleWith { console =>
Console.printLine("Hello") *> console.output
}
randomValue <- testRandomWith { random =>
random.setSeed(456L) *> Random.nextInt(100)
}
envVar <- testSystemWith { system =>
system.putEnv("VAR", "value") *> System.env("VAR")
}
} yield assertTrue(
output.contains("Hello") &&
randomValue >= 0 && randomValue < 100 &&
envVar.contains("value")
)
}Working with live services when test services are insufficient.
/**
* Execute effect with live clock instead of test clock
* @param zio effect to execute with live clock
* @return effect executed with live clock service
*/
def live[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]
/**
* Execute effect with live services replacing test services
* @param zio effect to execute with live services
* @return effect executed with live service implementations
*/
def withLive[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]Usage Examples:
import zio.test._
import zio._
// Mix test and live services
test("performance measurement with real time") {
for {
// Use test clock for most of the test
testClock <- ZIO.service[TestClock]
_ <- testClock.setTime(Duration.fromMillis(1000000L))
// Use live clock for actual timing measurement
start <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
_ <- live(ZIO.sleep(100.millis)) // Actually sleep for 100ms
end <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
duration = end - start
} yield assertTrue(duration >= 100L && duration < 200L)
}
// Test with real console for debugging
test("debug with real console") {
for {
_ <- Console.printLine("This goes to test console")
_ <- live(Console.printLine("This goes to real console for debugging"))
testConsole <- ZIO.service[TestConsole]
output <- testConsole.output
} yield assertTrue(output.size == 1) // Only test console output is captured
}Install with Tessl CLI
npx tessl i tessl/maven-dev-zio--zio-test-2-12