ZIO Test is a zero-dependency testing library that makes it easy to test effectual programs
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)
}Install with Tessl CLI
npx tessl i tessl/maven-dev-zio--zio-test-2-12