Controllable test environment services that replace real services with deterministic, testable implementations. Test services enable predictable testing of time-dependent, I/O-dependent, and stateful code.
Core test environment type and management functions.
/**
* Standard test environment combining all test services
*/
type TestEnvironment = Annotations with Live with Sized with TestConfig
object TestEnvironment {
/** Layer providing TestEnvironment from existing environment */
val any: ZLayer[TestEnvironment, Nothing, TestEnvironment]
/** Layer providing TestEnvironment from live services */
val live: ZLayer[Clock with Console with System with Random, Nothing, TestEnvironment]
}
/**
* Live environment layer providing real services
*/
val liveEnvironment: Layer[Nothing, Clock with Console with System with Random]
/**
* Test environment layer providing all test services
*/
val testEnvironment: ZLayer[Any, Nothing, TestEnvironment]Usage Examples:
import zio.test._
// Using test environment in specs
object MyTest extends ZIOSpecDefault {
def spec = suite("Test with environment")(
test("time-based test") {
for {
_ <- TestClock.adjust(1.hour)
time <- Clock.instant
} yield assertTrue(time.getEpochSecond > 0)
}
)
}
// Custom environment combining test and domain services
type MyTestEnv = TestEnvironment with MyService
val myTestLayer: ZLayer[Any, Nothing, MyTestEnv] =
testEnvironment ++ MyService.testControllable clock service for testing time-dependent operations.
/**
* Controllable clock for testing time-dependent code
*/
trait TestClock extends Clock {
/** Adjust the test clock by the specified duration */
def adjust(duration: Duration)(implicit trace: Trace): UIO[Unit]
/** Set the test clock to a specific instant */
def setTime(instant: Instant)(implicit trace: Trace): UIO[Unit]
/** Get all pending scheduled operations */
def sleeps(implicit trace: Trace): UIO[List[Duration]]
/** Save the current clock state */
def save(implicit trace: Trace): UIO[TestClock.Data]
/** Restore the clock to a previous state */
def restore(data: TestClock.Data)(implicit trace: Trace): UIO[Unit]
}
object TestClock {
/** Default TestClock layer */
val default: ZLayer[Any, Nothing, TestClock]
/** Data representing clock state for save/restore */
case class Data(instant: Instant, sleeps: List[Duration])
}
/**
* Access TestClock service
*/
def testClock(implicit trace: Trace): UIO[TestClock]
/**
* Access TestClock service and run workflow
*/
def testClockWith[R, E, A](f: TestClock => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Usage Examples:
import zio.test._
test("time-dependent operations") {
for {
// Start a timed operation
fiber <- ZIO.sleep(1.hour).fork
// Verify it hasn't completed yet
isDone <- fiber.isDone
_ <- assertTrue(!isDone)
// Advance time
_ <- TestClock.adjust(1.hour)
// Verify operation completed
_ <- fiber.join
isDone <- fiber.isDone
} yield assertTrue(isDone)
}
test("clock manipulation") {
for {
// Set specific time
_ <- TestClock.setTime(Instant.ofEpochSecond(0))
// Check current time
now <- Clock.instant
_ <- assertTrue(now.getEpochSecond == 0)
// Advance time by specific amount
_ <- TestClock.adjust(Duration.ofMinutes(30))
// Verify time advanced
later <- Clock.instant
} yield assertTrue(later.getEpochSecond == 30 * 60)
}Controllable console service for testing I/O operations.
/**
* Controllable console for testing I/O operations
*/
trait TestConsole extends Console {
/** Feed input to the console for reading */
def feedLines(lines: String*)(implicit trace: Trace): UIO[Unit]
/** Get all output written to console */
def output(implicit trace: Trace): UIO[Vector[String]]
/** Clear all console output */
def clearOutput(implicit trace: Trace): UIO[Unit]
/** Get all error output written to console */
def errorOutput(implicit trace: Trace): UIO[Vector[String]]
/** Clear all console error output */
def clearErrorOutput(implicit trace: Trace): UIO[Unit]
/** Save the current console state */
def save(implicit trace: Trace): UIO[TestConsole.Data]
/** Restore console to previous state */
def restore(data: TestConsole.Data)(implicit trace: Trace): UIO[Unit]
}
object TestConsole {
/** Default TestConsole layer */
val default: ZLayer[Any, Nothing, TestConsole]
/** Debug TestConsole layer that prints to real console */
val debug: ZLayer[Any, Nothing, TestConsole]
/** Data representing console state for save/restore */
case class Data(input: List[String], output: Vector[String], errorOutput: Vector[String])
}
/**
* Access TestConsole service
*/
def testConsole(implicit trace: Trace): UIO[TestConsole]
/**
* Access TestConsole service and run workflow
*/
def testConsoleWith[R, E, A](f: TestConsole => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Usage Examples:
import zio.test._
test("console input/output") {
for {
// Feed input for reading
_ <- TestConsole.feedLines("Alice", "25")
// Read input
name <- Console.readLine("Enter name: ")
age <- Console.readLine("Enter age: ")
// Write output
_ <- Console.printLine(s"Hello $name, you are $age years old")
// Verify output
output <- TestConsole.output
} yield assertTrue(
name == "Alice" &&
age == "25" &&
output.contains("Hello Alice, you are 25 years old")
)
}
test("error output") {
for {
// Write to error stream
_ <- Console.printLineError("Error occurred")
// Check error output
errors <- TestConsole.errorOutput
} yield assertTrue(errors.contains("Error occurred"))
}Controllable random number generator for deterministic testing.
/**
* Controllable random number generator for deterministic testing
*/
trait TestRandom extends Random {
/** Set the random seed */
def setSeed(seed: Long)(implicit trace: Trace): UIO[Unit]
/** Get the current random seed */
def getSeed(implicit trace: Trace): UIO[Long]
/** Feed specific values to be returned by random operations */
def feedInts(ints: Int*)(implicit trace: Trace): UIO[Unit]
def feedLongs(longs: Long*)(implicit trace: Trace): UIO[Unit]
def feedDoubles(doubles: Double*)(implicit trace: Trace): UIO[Unit]
def feedFloats(floats: Float*)(implicit trace: Trace): UIO[Unit]
def feedBooleans(booleans: Boolean*)(implicit trace: Trace): UIO[Unit]
def feedStrings(strings: String*)(implicit trace: Trace): UIO[Unit]
def feedUUIDs(uuids: java.util.UUID*)(implicit trace: Trace): UIO[Unit]
def feedBytes(bytes: Chunk[Byte]*)(implicit trace: Trace): UIO[Unit]
/** Clear all fed values */
def clearInts(implicit trace: Trace): UIO[Unit]
def clearLongs(implicit trace: Trace): UIO[Unit]
def clearDoubles(implicit trace: Trace): UIO[Unit]
def clearFloats(implicit trace: Trace): UIO[Unit]
def clearBooleans(implicit trace: Trace): UIO[Unit]
def clearStrings(implicit trace: Trace): UIO[Unit]
def clearUUIDs(implicit trace: Trace): UIO[Unit]
def clearBytes(implicit trace: Trace): UIO[Unit]
/** Save the current random state */
def save(implicit trace: Trace): UIO[TestRandom.Data]
/** Restore random to previous state */
def restore(data: TestRandom.Data)(implicit trace: Trace): UIO[Unit]
}
object TestRandom {
/** Deterministic TestRandom layer */
val deterministic: ZLayer[Any, Nothing, TestRandom]
/** TestRandom layer with specific seed */
def seeded(seed: Long): ZLayer[Any, Nothing, TestRandom]
/** Data representing random state for save/restore */
case class Data(seed: Long, ints: List[Int], /* other fed values */)
}
/**
* Access TestRandom service
*/
def testRandom(implicit trace: Trace): UIO[TestRandom]
/**
* Access TestRandom service and run workflow
*/
def testRandomWith[R, E, A](f: TestRandom => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Usage Examples:
import zio.test._
test("deterministic random values") {
for {
// Feed specific values
_ <- TestRandom.feedInts(42, 24, 7)
_ <- TestRandom.feedBooleans(true, false, true)
// Generate values (will use fed values)
n1 <- Random.nextInt
n2 <- Random.nextInt
n3 <- Random.nextInt
b1 <- Random.nextBoolean
b2 <- Random.nextBoolean
b3 <- Random.nextBoolean
} yield assertTrue(
n1 == 42 && n2 == 24 && n3 == 7 &&
b1 && !b2 && b3
)
}
test("seeded random") {
for {
// Set specific seed for reproducible tests
_ <- TestRandom.setSeed(12345L)
// Generate values (will be deterministic)
values <- ZIO.collectAll(List.fill(5)(Random.nextInt))
// Reset to same seed
_ <- TestRandom.setSeed(12345L)
// Generate same values again
sameValues <- ZIO.collectAll(List.fill(5)(Random.nextInt))
} yield assertTrue(values == sameValues)
}Controllable system environment for testing system interactions.
/**
* Controllable system environment for testing system interactions
*/
trait TestSystem extends System {
/** Set environment variable */
def putEnv(name: String, value: String)(implicit trace: Trace): UIO[Unit]
/** Remove environment variable */
def clearEnv(name: String)(implicit trace: Trace): UIO[Unit]
/** Set system property */
def putProperty(name: String, value: String)(implicit trace: Trace): UIO[Unit]
/** Remove system property */
def clearProperty(name: String)(implicit trace: Trace): UIO[Unit]
/** Set all environment variables */
def setEnvs(envs: Map[String, String])(implicit trace: Trace): UIO[Unit]
/** Set all system properties */
def setProperties(props: Map[String, String])(implicit trace: Trace): UIO[Unit]
/** Save the current system state */
def save(implicit trace: Trace): UIO[TestSystem.Data]
/** Restore system to previous state */
def restore(data: TestSystem.Data)(implicit trace: Trace): UIO[Unit]
}
object TestSystem {
/** Default TestSystem layer */
val default: ZLayer[Any, Nothing, TestSystem]
/** Data representing system state for save/restore */
case class Data(envs: Map[String, String], properties: Map[String, String])
}
/**
* Access TestSystem service
*/
def testSystem(implicit trace: Trace): UIO[TestSystem]
/**
* Access TestSystem service and run workflow
*/
def testSystemWith[R, E, A](f: TestSystem => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Usage Examples:
import zio.test._
test("environment variables") {
for {
// Set environment variable
_ <- TestSystem.putEnv("MY_VAR", "test_value")
// Read environment variable
value <- System.env("MY_VAR")
// Clear environment variable
_ <- TestSystem.clearEnv("MY_VAR")
// Verify it's gone
cleared <- System.env("MY_VAR")
} yield assertTrue(
value.contains("test_value") &&
cleared.isEmpty
)
}
test("system properties") {
for {
// Set system property
_ <- TestSystem.putProperty("test.prop", "test_value")
// Read system property
value <- System.property("test.prop")
// Clear system property
_ <- TestSystem.clearProperty("test.prop")
// Verify it's gone
cleared <- System.property("test.prop")
} yield assertTrue(
value.contains("test_value") &&
cleared.isEmpty
)
}Test configuration service for controlling test execution parameters.
/**
* Test configuration service
*/
trait TestConfig {
/** Number of samples for property-based tests */
def samples: Int
/** Maximum number of shrinking iterations */
def shrinks: Int
/** Repeats configuration */
def repeats: Int
/** Retries configuration */
def retries: Int
/** Test aspect to apply to property tests */
def checkAspect: TestAspectPoly
}
object TestConfig {
/** Live TestConfig layer with specified parameters */
def live(samples: Int, shrinks: Int, repeats: Int, retries: Int): ZLayer[Any, Nothing, TestConfig]
/** Default TestConfig layer */
val default: ZLayer[Any, Nothing, TestConfig]
}
/**
* Access TestConfig service
*/
def testConfig(implicit trace: Trace): UIO[TestConfig]
/**
* Access TestConfig service and run workflow
*/
def testConfigWith[R, E, A](f: TestConfig => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Service for managing test annotations and metadata.
/**
* Service for managing test annotations and metadata
*/
trait Annotations {
/** Get annotation value for the specified key */
def get[V](key: TestAnnotation[V])(implicit trace: Trace): UIO[V]
/** Annotate with key-value pair */
def annotate[V](key: TestAnnotation[V], value: V)(implicit trace: Trace): UIO[Unit]
/** Get all annotations as a map */
def annotated(implicit trace: Trace): UIO[TestAnnotationMap]
/** Run effect with additional annotation */
def withAnnotation[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]
}
object Annotations {
/** Live Annotations layer */
val live: ZLayer[Any, Nothing, Annotations]
}
/**
* Access Annotations service
*/
def annotations(implicit trace: Trace): UIO[Annotations]
/**
* Access Annotations service and run workflow
*/
def annotationsWith[R, E, A](f: Annotations => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Service providing size parameter for generators.
/**
* Service providing size parameter for generators
*/
trait Sized {
/** Get the current size parameter */
def size(implicit trace: Trace): UIO[Int]
}
object Sized {
/** Live Sized layer with specified size */
def live(size: Int): ZLayer[Any, Nothing, Sized]
}
/**
* Access Sized service
*/
def sized(implicit trace: Trace): UIO[Sized]
/**
* Access Sized service and run workflow
*/
def sizedWith[R, E, A](f: Sized => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]Service for accessing live environment during tests.
/**
* Service for accessing live environment during tests
*/
trait Live {
/** Run effect with live environment instead of test environment */
def live[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]
}
object Live {
/** Default Live layer */
val default: ZLayer[Any, Nothing, Live]
}
/**
* Access Live service
*/
def live(implicit trace: Trace): UIO[Live]
/**
* Access Live service and run workflow
*/
def liveWith[R, E, A](f: Live => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]
/**
* Run effect with live environment
*/
def live[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A]
/**
* Transform effect with live environment access
*/
def withLive[R, E, E1, A, B](zio: ZIO[R, E, A])(f: ZIO[R, E, A] => ZIO[R, E1, B])(implicit trace: Trace): ZIO[R, E1, B]Functions for managing and customizing test services.
/**
* Run workflow with custom annotations service
*/
def withAnnotations[R, E, A <: Annotations, B](annotations: => A)(zio: => ZIO[R, E, B])(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B]
/**
* Set annotations service in scope
*/
def withAnnotationsScoped[A <: Annotations](annotations: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit]
/**
* Run workflow with custom config service
*/
def withTestConfig[R, E, A <: TestConfig, B](testConfig: => A)(zio: => ZIO[R, E, B])(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B]
/**
* Set config service in scope
*/
def withTestConfigScoped[A <: TestConfig](testConfig: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit]
/**
* Run workflow with custom sized service
*/
def withSized[R, E, A <: Sized, B](sized: => A)(zio: => ZIO[R, E, B])(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B]
/**
* Set sized service in scope
*/
def withSizedScoped[A <: Sized](sized: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit]
/**
* Run workflow with custom live service
*/
def withLive[R, E, A <: Live, B](live: => A)(zio: => ZIO[R, E, B])(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B]
/**
* Set live service in scope
*/
def withLiveScoped[A <: Live](live: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit]Usage Examples:
import zio.test._
test("custom test configuration") {
val customConfig = new TestConfig {
def samples = 1000 // More samples than default
def shrinks = 50 // More shrinking iterations
def repeats = 1
def retries = 1
def checkAspect = TestAspect.identity
}
withTestConfig(customConfig) {
check(Gen.int) { n =>
assertTrue(n.isInstanceOf[Int])
}
}
}
test("custom size parameter") {
val largeSize = new Sized {
def size(implicit trace: Trace) = ZIO.succeed(200)
}
withSized(largeSize) {
check(Gen.sized(n => Gen.listOfN(n)(Gen.int))) { numbers =>
assertTrue(numbers.size <= 200)
}
}
}