ZIO Test is a featherweight testing library for effectful programs built on ZIO, providing a comprehensive framework for property-based testing, test composition, and resource-safe test execution.
Cross-cutting concerns for configuring test execution, lifecycle, timeouts, retries, and environment management. Test aspects provide a powerful way to modify test behavior without changing test logic.
The fundamental test aspect type for transforming test specifications.
/**
* Base class for test aspects that transform test specifications
* @tparam R0 - Input environment requirement
* @tparam R1 - Output environment requirement
* @tparam E0 - Input error type
* @tparam E1 - Output error type
*/
abstract class TestAspect[-R0, +R1, -E0, +E1] {
/** Apply this aspect to a test specification */
def apply[R <: R0, E >: E0](spec: Spec[R, E]): Spec[R1, E1]
/** Compose this aspect with another aspect */
def >>>[R2 >: R1, R3, E2 >: E1, E3](that: TestAspect[R2, R3, E2, E3]): TestAspect[R0, R3, E0, E3]
/** Compose another aspect with this aspect */
def <<<[R2, E2](that: TestAspect[R2, R0, E2, E0]): TestAspect[R2, R1, E2, E1]
/** Combine aspects with logical AND */
def &&[R2 <: R0, R3 >: R1, E2 >: E0, E3 >: E1](that: TestAspect[R2, R3, E2, E3]): TestAspect[R2, R3, E2, E3]
}
/**
* Type aliases for common aspect patterns
*/
type TestAspectPoly = TestAspect[Nothing, Any, Nothing, Any]
type TestAspectAtLeastR[-R] = TestAspect[Nothing, R, Nothing, Any]Essential aspects for common test scenarios.
/**
* Identity aspect that does nothing
*/
def identity: TestAspectPoly
/**
* Ignore all tests (mark as ignored)
*/
def ignore: TestAspectPoly
/**
* Retry flaky tests up to n times until they pass
* @param n - Maximum number of retries
*/
def flaky(n: Int): TestAspectPoly
/**
* Mark tests as non-flaky (disable retries)
* @param n - Number of times to mark as non-flaky
*/
def nonFlaky(n: Int): TestAspectPoly
/**
* Repeat tests n times
* @param n - Number of repetitions
*/
def repeats(n: Int): TestAspectPoly
/**
* Retry failed tests up to n times
* @param n - Maximum number of retries
*/
def retries(n: Int): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Apply aspects using @@ syntax
test("flaky test") {
// Test that might fail occasionally
assertTrue(scala.util.Random.nextBoolean())
} @@ flaky(3)
test("repeated test") {
// Test that should run multiple times
assertTrue(1 + 1 == 2)
} @@ repeats(5)
// Ignore specific tests
test("work in progress") {
assertTrue(false) // This won't run
} @@ ignoreAspects for controlling test execution time limits.
/**
* Set timeout for test execution
* @param duration - Maximum execution time
*/
def timeout(duration: Duration): TestAspectPoly
/**
* Set timeout with warning before timeout
* @param duration - Maximum execution time
*/
def timeoutWarning(duration: Duration): TestAspectPoly
/**
* Detect non-terminating tests
* @param duration - Time after which to consider test non-terminating
*/
def nonTermination(duration: Duration): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
test("long running operation") {
ZIO.sleep(30.seconds)
} @@ timeout(1.minute)
test("operation with warning") {
ZIO.sleep(45.seconds)
} @@ timeoutWarning(1.minute)
suite("time-sensitive tests")(
test("test 1") { assertTrue(true) },
test("test 2") { assertTrue(true) }
) @@ timeout(30.seconds) // Apply to entire suiteAspects controlling parallel vs sequential execution.
/**
* Execute tests sequentially
*/
def sequential: TestAspectPoly
/**
* Execute tests in parallel
*/
def parallel: TestAspectPoly
/**
* Execute tests in parallel with specific number of threads
* @param n - Number of parallel threads
*/
def parallelN(n: Int): TestAspectPoly
/**
* Use custom execution strategy
* @param strategy - Execution strategy to use
*/
def executionStrategy(strategy: ExecutionStrategy): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Run entire suite in parallel
suite("parallel tests")(
test("test 1") { ZIO.sleep(1.second) *> assertTrue(true) },
test("test 2") { ZIO.sleep(1.second) *> assertTrue(true) },
test("test 3") { ZIO.sleep(1.second) *> assertTrue(true) }
) @@ parallel
// Force sequential execution for specific suite
suite("sequential tests")(
test("setup") { setupTest() },
test("main") { mainTest() },
test("cleanup") { cleanupTest() }
) @@ sequential
// Limit parallelism
suite("limited parallel")(
// Many tests...
) @@ parallelN(4)Aspects for managing test setup and teardown.
/**
* Run effect before each test
* @param zio - Effect to run before each test
*/
def before[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after each test
* @param zio - Effect to run after each test
*/
def after[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effects before and after each test
* @param before - Effect to run before each test
* @param after - Effect to run after each test
*/
def around[R](before: ZIO[R, Nothing, Any], after: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect before all tests in a suite
* @param zio - Effect to run before all tests
*/
def beforeAll[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after all tests in a suite
* @param zio - Effect to run after all tests
*/
def afterAll[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effects before and after all tests in a suite
* @param before - Effect to run before all tests
* @param after - Effect to run after all tests
*/
def aroundAll[R](before: ZIO[R, Nothing, Any], after: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after successful test
* @param zio - Effect to run after test success
*/
def afterSuccess[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after failed test
* @param zio - Effect to run after test failure
*/
def afterFailure[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after all tests succeed
* @param zio - Effect to run after all tests succeed
*/
def afterAllSuccess[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]
/**
* Run effect after any test fails
* @param zio - Effect to run after any test fails
*/
def afterAllFailure[R](zio: ZIO[R, Nothing, Any]): TestAspect[Nothing, R, Nothing, Any]Usage Examples:
import zio.test._
import zio.test.TestAspect._
// Database test with setup/teardown
suite("database tests")(
test("create user") { createUserTest() },
test("read user") { readUserTest() },
test("update user") { updateUserTest() },
test("delete user") { deleteUserTest() }
) @@ beforeAll(initializeDatabase()) @@ afterAll(cleanupDatabase())
// Individual test setup
test("file processing") {
processFile("test.txt")
} @@ before(createTestFile()) @@ after(deleteTestFile())
// Conditional cleanup
test("resource test") {
useResource()
} @@ afterSuccess(logSuccess()) @@ afterFailure(logFailure())Aspects for providing dependencies and environment to tests.
/**
* Provide layer to tests
* @param layer - ZLayer to provide
*/
def fromLayer[R](layer: ZLayer[Any, Nothing, R]): TestAspect[Nothing, R, Nothing, Any]
/**
* Provide shared layer to tests (layer is created once and reused)
* @param layer - ZLayer to provide and share
*/
def fromLayerShared[R](layer: ZLayer[Any, Nothing, R]): TestAspect[Nothing, R, Nothing, Any]
/**
* Use custom config provider
* @param provider - ConfigProvider to use
*/
def withConfigProvider(provider: ConfigProvider): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Provide database layer to tests
suite("service tests")(
test("user service") {
for {
service <- ZIO.service[UserService]
user <- service.createUser("Alice")
} yield assertTrue(user.name == "Alice")
}
) @@ fromLayer(DatabaseLayer.test ++ UserService.live)
// Shared expensive resource
suite("integration tests")(
// Multiple tests using shared database connection
) @@ fromLayerShared(DatabaseConnectionPool.live)
// Custom configuration
suite("config tests")(
test("load config") {
for {
config <- ZIO.config[AppConfig]
} yield assertTrue(config.appName.nonEmpty)
}
) @@ withConfigProvider(ConfigProvider.fromMap(Map("appName" -> "TestApp")))Aspects for configuring property-based test parameters.
/**
* Set number of samples for property tests
* @param n - Number of samples to generate
*/
def samples(n: Int): TestAspectPoly
/**
* Set number of shrinking attempts
* @param n - Maximum shrinking iterations
*/
def shrinks(n: Int): TestAspectPoly
/**
* Set generator size parameter
* @param n - Size parameter for generators
*/
def size(n: Int): TestAspectPoly
/**
* Set generator size using function
* @param f - Function to transform size parameter
*/
def sized(f: Int => Int): TestAspectPoly
/**
* Set random seed for reproducible tests
* @param seed - Random seed value
*/
def setSeed(seed: Long): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
test("property test with more samples") {
check(Gen.int) { n =>
assertTrue((n + 1) > n)
}
} @@ samples(10000)
test("property test with larger values") {
check(Gen.listOf(Gen.int)) { numbers =>
assertTrue(numbers.reverse.reverse == numbers)
}
} @@ size(1000)
test("reproducible property test") {
check(Gen.int) { n =>
assertTrue(n.isInstanceOf[Int])
}
} @@ setSeed(42L)Aspects for conditional test execution based on platform or environment.
/**
* Run only if environment variable matches condition
* @param name - Environment variable name
* @param predicate - Condition to check variable value
*/
def ifEnv(name: String, predicate: String => Boolean): TestAspectPoly
/**
* Run only if environment variable is set
* @param name - Environment variable name
*/
def ifEnvSet(name: String): TestAspectPoly
/**
* Run only if environment variable is not set
* @param name - Environment variable name
*/
def ifEnvNotSet(name: String): TestAspectPoly
/**
* Run conditionally based on environment variable option
* @param name - Environment variable name
* @param predicate - Condition to check optional value
*/
def ifEnvOption(name: String, predicate: Option[String] => Boolean): TestAspectPoly
/**
* Run only if system property matches condition
* @param name - System property name
* @param predicate - Condition to check property value
*/
def ifProp(name: String, predicate: String => Boolean): TestAspectPoly
/**
* Run only if system property is set
* @param name - System property name
*/
def ifPropSet(name: String): TestAspectPoly
/**
* Run only if system property is not set
* @param name - System property name
*/
def ifPropNotSet(name: String): TestAspectPoly
/**
* Run conditionally based on system property option
* @param name - System property name
* @param predicate - Condition to check optional value
*/
def ifPropOption(name: String, predicate: Option[String] => Boolean): TestAspectPoly
/**
* Run only on JVM platform
*/
def jvmOnly: TestAspectPoly
/**
* Run only on JavaScript platform
*/
def jsOnly: TestAspectPoly
/**
* Run only on Scala Native platform
*/
def nativeOnly: TestAspectPoly
/**
* Run only on Unix systems
*/
def unix: TestAspectPoly
/**
* Run only on Windows systems
*/
def windows: TestAspectPoly
/**
* Run conditionally based on operating system
* @param predicate - Condition to check OS name
*/
def os(predicate: String => Boolean): TestAspectPoly
/**
* Run only on Scala 2
*/
def scala2Only: TestAspectPoly
/**
* Run only on Scala 3
*/
def scala3Only: TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Platform-specific tests
test("JVM-specific functionality") {
// JVM-only code
assertTrue(System.getProperty("java.version").nonEmpty)
} @@ jvmOnly
test("Unix file permissions") {
// Unix-specific test
assertTrue(java.io.File.separatorChar == '/')
} @@ unix
// Environment-conditional tests
test("production database test") {
// Only run if ENVIRONMENT=production
connectToDatabase()
} @@ ifEnv("ENVIRONMENT", _ == "production")
test("CI-only test") {
// Only run in CI environment
runExpensiveTest()
} @@ ifEnvSet("CI")
// Scala version specific
test("Scala 3 feature") {
// Test Scala 3 specific features
assertTrue(true)
} @@ scala3OnlyAspects for saving and restoring test service state.
/**
* Restore using custom restorable
* @param restorable - Custom restorable implementation
*/
def restore[R](restorable: Restorable[R]): TestAspect[Nothing, R, Nothing, Any]
/**
* Restore test clock state after each test
*/
def restoreTestClock: TestAspectPoly
/**
* Restore test console state after each test
*/
def restoreTestConsole: TestAspectPoly
/**
* Restore test random state after each test
*/
def restoreTestRandom: TestAspectPoly
/**
* Restore test system state after each test
*/
def restoreTestSystem: TestAspectPoly
/**
* Restore entire test environment state after each test
*/
def restoreTestEnvironment: TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
suite("clock tests")(
test("advance time") {
TestClock.adjust(1.hour) *>
assertTrue(true)
},
test("time is reset") {
// Clock state restored from previous test
for {
time <- Clock.instant
} yield assertTrue(time.getEpochSecond == 0)
}
) @@ restoreTestClock
suite("console tests")(
test("write output") {
Console.printLine("test output") *>
assertTrue(true)
},
test("output is cleared") {
// Console state restored from previous test
for {
output <- TestConsole.output
} yield assertTrue(output.isEmpty)
}
) @@ restoreTestConsoleAspects for debugging and analyzing test execution.
/**
* Add diagnostic information to test execution
* @param duration - Duration to wait before showing diagnostics
*/
def diagnose(duration: Duration): TestAspectPoly
/**
* Suppress test output (run silently)
*/
def silent: TestAspectPoly
/**
* Add tags to tests for filtering and organization
* @param tag - Primary tag
* @param tags - Additional tags
*/
def tag(tag: String, tags: String*): TestAspectPolyAspects for adding verification conditions to tests.
/**
* Add verification condition to test results
* @param assertion - Assertion to verify against test result
*/
def verify(assertion: Assertion[TestResult]): TestAspectPoly
/**
* Expect test to fail with specific condition
* @param assertion - Assertion to verify against test failure
*/
def failing[E](assertion: Assertion[TestFailure[E]]): TestAspect[Nothing, Any, E, Nothing]Usage Examples:
import zio.test._
import zio.test.TestAspect._
test("expected failure") {
// This test is expected to fail
assertTrue(false)
} @@ failing(anything)
test("performance verification") {
expensiveOperation()
} @@ verify(anything) @@ diagnose(5.seconds)
// Tagged tests for organization
suite("integration tests")(
test("database test") { dbTest() } @@ tag("db", "integration"),
test("api test") { apiTest() } @@ tag("api", "integration"),
test("auth test") { authTest() } @@ tag("auth", "integration")
)Methods for combining and composing aspects.
// Combine aspects using && operator
val combinedAspect = timeout(30.seconds) && parallel && samples(1000)
// Chain aspects using >>> operator
val chainedAspect = beforeAll(setup()) >>> timeout(1.minute) >>> afterAll(cleanup())
// Apply multiple aspects to test
test("complex test") {
complexOperation()
} @@ timeout(1.minute) @@ flaky(3) @@ restoreTestClock @@ tag("slow", "integration")Usage Examples:
import zio.test._
import zio.test.TestAspect._
// Complex aspect composition
val integrationTestAspect =
beforeAll(setupIntegrationEnvironment()) @@
afterAll(cleanupIntegrationEnvironment()) @@
timeout(5.minutes) @@
flaky(2) @@
tag("integration") @@
ifEnvSet("RUN_INTEGRATION_TESTS")
suite("integration tests")(
test("end-to-end workflow") { e2eTest() },
test("external service integration") { externalServiceTest() }
) @@ integrationTestAspectInstall with Tessl CLI
npx tessl i tessl/maven-dev-zio--zio-test-3