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
}