Configuration system for controlling test execution parameters and environment setup for dependency injection.
Configuration service that controls various aspects of test execution.
/**
* Configuration for test execution parameters
*/
trait TestConfig {
/**
* Number of times to repeat each test
* @return number of repetitions
*/
def repeats: Int
/**
* Number of times to retry failed tests
* @return number of retries
*/
def retries: Int
/**
* Number of samples for property-based tests
* @return number of samples to generate
*/
def samples: Int
/**
* Maximum shrinking attempts for failed property tests
* @return maximum shrinking attempts
*/
def shrinks: Int
/**
* Size parameter for generators
* @return size value for Sized environment
*/
def size: Int
}
object TestConfig {
/**
* Create test configuration with specified parameters
* @param repeats number of test repetitions
* @param retries number of retry attempts
* @param samples number of property test samples
* @param shrinks maximum shrinking attempts
* @param size generator size parameter
* @return layer providing test configuration
*/
def live(
repeats: Int,
retries: Int,
samples: Int,
shrinks: Int,
size: Int
): ULayer[TestConfig]
/**
* Default test configuration
* - repeats: 1
* - retries: 0
* - samples: 200
* - shrinks: 1000
* - size: 100
*/
val default: ULayer[TestConfig]
/**
* Access number of repeats from configuration
*/
val repeats: URIO[TestConfig, Int]
/**
* Access number of retries from configuration
*/
val retries: URIO[TestConfig, Int]
/**
* Access number of samples from configuration
*/
val samples: URIO[TestConfig, Int]
/**
* Access number of shrinks from configuration
*/
val shrinks: URIO[TestConfig, Int]
/**
* Access size parameter from configuration
*/
val size: URIO[TestConfig, Int]
}Usage Examples:
import zio.test._
import zio._
// Custom test configuration
val customConfig = TestConfig.live(
repeats = 5, // Run each test 5 times
retries = 2, // Retry failed tests twice
samples = 1000, // Use 1000 samples for property tests
shrinks = 500, // Maximum 500 shrinking attempts
size = 200 // Generator size of 200
)
// Using configuration in tests
test("property test with custom config").provideLayer(customConfig) {
check(Gen.listOf(Gen.anyInt)) { list =>
assertTrue(list.reverse.reverse == list)
}
}
// Access configuration values
test("configuration access") {
for {
samples <- TestConfig.samples
size <- TestConfig.size
} yield assertTrue(samples > 0 && size > 0)
}Combined environment type containing all standard test services.
/**
* Standard test environment combining all test services
*/
type TestEnvironment = Annotations with Live with Sized with TestConfig
object TestEnvironment {
/**
* Create test environment from live system services
* @return layer providing complete test environment
*/
val live: ZLayer[Clock with Console with System with Random, Nothing, TestEnvironment]
/**
* Access any service from test environment
* @return layer that provides access to the environment
*/
val any: ZLayer[TestEnvironment, Nothing, TestEnvironment]
}
/**
* Layer providing live system services for test environment creation
*/
val liveEnvironment: Layer[Nothing, Clock with Console with System with Random]
/**
* Complete test environment layer (recommended for most tests)
*/
val testEnvironment: ZLayer[Any, Nothing, TestEnvironment]Usage Examples:
import zio.test._
import zio._
// Using default test environment
object MyTestSpec extends ZIOSpecDefault {
def spec = suite("My Tests")(
test("basic test") {
// Has access to full TestEnvironment
for {
time <- Clock.currentTime(TimeUnit.MILLISECONDS)
_ <- Console.printLine(s"Current time: $time")
random <- Random.nextInt(100)
} yield assertTrue(time > 0 && random >= 0 && random < 100)
}
)
}
// Custom environment combination
val customTestEnv: ZLayer[Any, Nothing, TestEnvironment] =
ZLayer.make[TestEnvironment](
TestClock.default,
TestConsole.debug,
TestRandom.deterministic(12345L),
TestSystem.default,
Annotations.live,
Live.default,
Sized.live(50),
TestConfig.live(1, 0, 500, 1000, 50)
)
object CustomEnvSpec extends ZIOSpec[TestEnvironment] {
def spec = suite("Custom Environment Tests")(
test("uses custom environment") {
for {
size <- Sized.size
samples <- TestConfig.samples
} yield assertTrue(size == 50 && samples == 500)
}
).provideLayer(customTestEnv)
}Command-line argument parsing for test runners.
/**
* Command-line arguments for test execution
*/
case class TestArgs(
testSearchTerms: List[String] = Nil,
tagSearchTerms: List[String] = Nil,
testTaskPolicy: TestTaskPolicy = TestTaskPolicy.Sequential,
// Execution parameters
repeats: Option[Int] = None,
retries: Option[Int] = None,
samples: Option[Int] = None,
shrinks: Option[Int] = None,
size: Option[Int] = None,
// Output control
verbose: Boolean = false,
color: Boolean = true,
summary: Boolean = true
)
object TestArgs {
/**
* Parse command-line arguments
* @param args command-line argument list
* @return parsed test arguments
*/
def parse(args: List[String]): Either[String, TestArgs]
/**
* Parse command-line arguments with defaults
* @param args command-line argument list
* @param defaults default test arguments
* @return parsed test arguments
*/
def parse(args: List[String], defaults: TestArgs): Either[String, TestArgs]
/**
* Empty test arguments (all defaults)
*/
val empty: TestArgs
}
/**
* Test execution policy
*/
sealed trait TestTaskPolicy
object TestTaskPolicy {
case object Sequential extends TestTaskPolicy
case object Parallel extends TestTaskPolicy
case class ParallelN(n: Int) extends TestTaskPolicy
}Usage Examples:
import zio.test._
// Parsing command-line arguments
val args = List("--samples", "1000", "--parallel", "--verbose", "UserTests")
val testArgs = TestArgs.parse(args) match {
case Right(args) => args
case Left(error) => throw new IllegalArgumentException(error)
}
// Using parsed arguments to configure tests
val configFromArgs = testArgs.samples.fold(TestConfig.default) { samples =>
TestConfig.live(
repeats = testArgs.repeats.getOrElse(1),
retries = testArgs.retries.getOrElse(0),
samples = samples,
shrinks = testArgs.shrinks.getOrElse(1000),
size = testArgs.size.getOrElse(100)
)
}Service for attaching metadata to tests.
/**
* Service for managing test annotations and metadata
*/
trait Annotations {
/**
* Get annotation value by key
* @param key annotation key
* @return effect producing optional annotation value
*/
def get[V](key: TestAnnotation[V]): UIO[V]
/**
* Annotate with key-value pair
* @param key annotation key
* @param value annotation value
* @return effect that adds the annotation
*/
def annotate[V](key: TestAnnotation[V], value: V): UIO[Unit]
/**
* Get all current annotations
* @return effect producing annotation map
*/
def annotationMap: UIO[TestAnnotationMap]
/**
* Supervise effect with annotation inheritance
* @param zio effect to supervise
* @return supervised effect with annotations
*/
def supervisedFibers[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]
}
object Annotations {
/**
* Live annotations implementation
*/
val live: ULayer[Annotations]
}
/**
* Test annotation key-value pair
*/
trait TestAnnotation[V] {
/**
* Annotation identifier
*/
def identifier: String
/**
* Initial/default value
*/
def initial: V
/**
* Combine two annotation values
* @param v1 first value
* @param v2 second value
* @return combined value
*/
def combine(v1: V, v2: V): V
}
object TestAnnotation {
/**
* Create custom annotation
* @param name annotation name
* @param initialValue initial value
* @param combineFunction value combination function
* @return annotation instance
*/
def apply[V](
name: String,
initialValue: V,
combineFunction: (V, V) => V
): TestAnnotation[V]
// Built-in annotations
val ignored: TestAnnotation[Boolean]
val repeated: TestAnnotation[Int]
val retried: TestAnnotation[Int]
val tagged: TestAnnotation[Set[String]]
val timing: TestAnnotation[Duration]
}Usage Examples:
import zio.test._
import zio._
// Using built-in annotations
test("annotated test") {
for {
_ <- Annotations.annotate(TestAnnotation.tagged, Set("slow", "integration"))
_ <- Annotations.annotate(TestAnnotation.repeated, 5)
annotations <- Annotations.annotationMap
} yield assertTrue(annotations.get(TestAnnotation.tagged).contains("slow"))
}
// Custom annotations
val databaseAnnotation = TestAnnotation[String](
"database",
"none",
(_, newer) => newer // Use newer value
)
test("database test") {
for {
_ <- Annotations.annotate(databaseAnnotation, "postgresql")
dbType <- Annotations.get(databaseAnnotation)
} yield assertTrue(dbType == "postgresql")
}Service for executing effects with live (production) implementations.
/**
* Service for accessing live implementations of system services
*/
trait Live {
/**
* Execute effect with live service implementations
* @param zio effect to execute with live services
* @return effect executed with live services
*/
def provide[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]
}
object Live {
/**
* Default live service implementation
*/
val default: ULayer[Live]
/**
* Execute effect with live services (convenience function)
* @param zio effect to execute
* @return effect with live services
*/
def live[R, E, A](zio: ZIO[R, E, A]): ZIO[R with Live, E, A]
/**
* Transform effect to use live services for outer effect while preserving
* test environment for inner effect
* @param zio inner effect using test environment
* @param f transformation using live environment
* @return transformed effect
*/
def withLive[R, E, E1, A, B](zio: ZIO[R, E, A])(
f: ZIO[R, E, A] => ZIO[R, E1, B]
): ZIO[R with Live, E1, B]
}Usage Examples:
import zio.test._
import zio._
// Execute with live services when needed
test("performance test with real time") {
for {
// Use test clock for setup
testClock <- ZIO.service[TestClock]
_ <- testClock.setTime(Duration.zero)
// Use live clock for actual measurement
result <- Live.live(
for {
start <- Clock.currentTime(TimeUnit.MILLISECONDS)
_ <- ZIO.sleep(100.millis) // Actually sleep
end <- Clock.currentTime(TimeUnit.MILLISECONDS)
} yield end - start
)
} yield assertTrue(result >= 100L) // Real time elapsed
}
// Mix test and live environments
test("hybrid test environment") {
Live.withLive(
// This runs with test environment
for {
_ <- Console.printLine("Test output") // Goes to test console
value <- Random.nextInt(100) // Uses test random
} yield value
) { effect =>
// This transformation uses live environment
for {
_ <- Console.printLine("Live output") // Goes to real console
result <- effect // Execute inner effect with test env
} yield result
}
}Functions for working with and modifying test environments.
/**
* Execute with modified annotations
* @param annotations new annotations service
* @param zio effect to execute
* @return effect with modified annotations
*/
def withAnnotations[R, E, A <: Annotations, B](annotations: => A)(
zio: => ZIO[R, E, B]
): ZIO[R, E, B]
/**
* Execute with modified test configuration
* @param testConfig new test configuration
* @param zio effect to execute
* @return effect with modified configuration
*/
def withTestConfig[R, E, A <: TestConfig, B](testConfig: => A)(
zio: => ZIO[R, E, B]
): ZIO[R, E, B]
/**
* Execute with modified size configuration
* @param sized new sized service
* @param zio effect to execute
* @return effect with modified size
*/
def withSized[R, E, A <: Sized, B](sized: => A)(
zio: => ZIO[R, E, B]
): ZIO[R, E, B]
/**
* Execute with modified live service
* @param live new live service
* @param zio effect to execute
* @return effect with modified live service
*/
def withLive[R, E, A <: Live, B](live: => A)(
zio: => ZIO[R, E, B]
): ZIO[R, E, B]Usage Examples:
import zio.test._
import zio._
// Temporarily modify test configuration
test("high sample count test") {
withTestConfig(TestConfig.live(1, 0, 10000, 5000, 200)) {
check(Gen.listOf(Gen.anyInt)) { list =>
assertTrue(list.reverse.reverse == list)
}
}
}
// Temporarily modify generator size
test("large data structures") {
withSized(Sized(1000)) {
check(Gen.listOf(Gen.anyString)) { largeList =>
assertTrue(largeList.size <= 1000)
}
}
}
// Environment scoped modifications
test("scoped environment changes") {
for {
// Normal size
normalList <- Gen.listOf(Gen.anyInt).sample.runHead
_ <- ZIO.scoped {
withSizedScoped(Sized(10)) {
// Large size in this scope
Gen.listOf(Gen.anyInt).sample.runHead.map { largeList =>
// Compare sizes
assertTrue(largeList.exists(_.size > normalList.map(_.size).getOrElse(0)))
}
}
}
} yield assertTrue(true)
}