Cross-cutting concerns for test execution including timeouts, retries, parallelism, and platform-specific behavior.
Core trait for aspects that modify test execution behavior.
/**
* A test aspect that can transform test specifications
* @tparam LowerR minimum environment requirement
* @tparam UpperR maximum environment requirement
* @tparam LowerE minimum error type
* @tparam UpperE maximum error type
*/
trait TestAspect[-LowerR, +UpperR, -LowerE, +UpperE] {
/**
* Apply this aspect to a test specification
* @param spec the test specification to transform
* @return transformed specification
*/
def apply[R >: LowerR <: UpperR, E >: LowerE <: UpperE](
spec: Spec[R, E]
): Spec[R, E]
/**
* Compose with another test aspect
* @param that other aspect to compose with
* @return composed aspect
*/
def @@[LowerR1 <: LowerR, UpperR1 >: UpperR, LowerE1 <: LowerE, UpperE1 >: UpperE](
that: TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
): TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
}
// Type aliases for common patterns
type TestAspectPoly = TestAspect[Nothing, Any, Nothing, Any]
type TestAspectAtLeastR[-R] = TestAspect[Nothing, R, Nothing, Any]Aspects for controlling test timing, timeouts, and execution behavior.
/**
* Apply timeout to test execution
* @param duration maximum duration before timeout
* @return aspect that times out tests exceeding duration
*/
def timeout(duration: Duration): TestAspectAtLeastR[Live]
/**
* Retry failing tests with exponential backoff
* @param n maximum number of retry attempts
* @return aspect that retries failed tests
*/
def retry(n: Int): TestAspectPoly
/**
* Retry tests until they succeed or max attempts reached
* Uses exponential backoff and jittering for better behavior
*/
val eventually: TestAspectAtLeastR[Live]
/**
* Mark tests as flaky (expected to fail intermittently)
* Flaky tests that fail are reported differently
*/
val flaky: TestAspectPoly
/**
* Mark test as non-flaky (opposite of flaky)
* Ensures test failures will fail the build
*/
val nonFlaky: TestAspectPoly
/**
* Mark tests to be ignored (not executed)
* Ignored tests are skipped but reported in results
*/
val ignore: TestAspectPoly
/**
* Repeat test execution N times
* @param n number of times to repeat the test
*/
def repeats(n: Int): TestAspectPoly
/**
* Execute tests with live clock instead of test clock
* Useful for tests that need real time progression
*/
val withLiveClock: TestAspectAtLeastR[Live]
/**
* Execute tests with live console instead of test console
* Useful for tests that need real console I/O
*/
val withLiveConsole: TestAspectAtLeastR[Live]
/**
* Execute tests with live random instead of test random
* Useful for tests that need non-deterministic randomness
*/
val withLiveRandom: TestAspectAtLeastR[Live]
/**
* Execute tests with live system instead of test system
* Useful for tests that need real system environment
*/
val withLiveSystem: TestAspectAtLeastR[Live]Usage Examples:
import zio.test._
import zio.test.TestAspect._
// Timeout aspects
test("fast operation") {
ZIO.sleep(100.millis) *> assertTrue(true)
} @@ timeout(1.second)
suite("API Tests")(
test("get users") { /* test logic */ },
test("create user") { /* test logic */ }
) @@ timeout(30.seconds)
// Retry and flaky tests
test("external service call") {
// Might fail due to network issues
externalService.getData.map(data => assertTrue(data.nonEmpty))
} @@ eventually
test("timing sensitive operation") {
// Occasionally fails due to timing
timingSensitiveOperation
} @@ flaky @@ retry(3)
// Live service aspects
test("actual time measurement") {
for {
start <- Clock.currentTime(TimeUnit.MILLISECONDS)
_ <- ZIO.sleep(100.millis)
end <- Clock.currentTime(TimeUnit.MILLISECONDS)
} yield assertTrue(end - start >= 100)
} @@ withLiveClockAspects controlling how tests are executed relative to each other.
/**
* Execute child tests in parallel
* Tests within the same suite run concurrently
*/
val parallel: TestAspectPoly
/**
* Execute child tests sequentially (default behavior)
* Tests run one after another in order
*/
val sequential: TestAspectPoly
/**
* Control parallelism level for test execution
* @param n maximum number of concurrent tests
*/
def parallelN(n: Int): TestAspectPoly
/**
* Execute each test in an isolated fiber
* Provides better isolation but more overhead
*/
val fibers: TestAspectPoly
/**
* Execute tests on a specific executor
* @param executor custom executor for test execution
*/
def executor(executor: Executor): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Parallel execution
suite("Independent Tests")(
test("test A") { /* independent test */ },
test("test B") { /* independent test */ },
test("test C") { /* independent test */ }
) @@ parallel
// Sequential execution (explicit)
suite("Dependent Tests")(
test("setup") { /* setup state */ },
test("use setup") { /* depends on setup */ },
test("cleanup") { /* cleanup state */ }
) @@ sequential
// Limited parallelism
suite("Database Tests")(
test("query 1") { /* database query */ },
test("query 2") { /* database query */ },
test("query 3") { /* database query */ }
) @@ parallelN(2) // At most 2 concurrent queries
// Custom executor
suite("CPU Intensive Tests")(
test("heavy computation") { /* CPU bound test */ }
) @@ executor(Runtime.defaultExecutor)Aspects for controlling test execution based on platform or environment conditions.
/**
* Execute only on JVM platform
*/
val jvm: TestAspectPoly
/**
* Execute only on Scala.js platform
*/
val js: TestAspectPoly
/**
* Execute only on Scala Native platform
*/
val native: TestAspectPoly
/**
* Execute only on Unix/Linux platforms
*/
val unix: TestAspectPoly
/**
* Execute only on Windows platforms
*/
val windows: TestAspectPoly
/**
* Execute based on system property condition
* @param property system property name
* @param value expected property value
*/
def ifProp(property: String, value: String): TestAspectPoly
/**
* Execute based on environment variable condition
* @param variable environment variable name
* @param value expected variable value
*/
def ifEnv(variable: String, value: String): TestAspectPoly
/**
* Execute based on custom condition
* @param condition predicate determining execution
*/
def conditional(condition: Boolean): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Platform-specific tests
suite("File System Tests")(
test("Unix file permissions") {
/* Unix-specific file operations */
} @@ unix,
test("Windows file paths") {
/* Windows-specific path handling */
} @@ windows,
test("Cross-platform file I/O") {
/* Works on all platforms */
}
)
// JavaScript-specific tests
suite("Browser Tests")(
test("DOM manipulation") {
/* Browser-specific functionality */
} @@ js,
test("Node.js file system") {
/* Node.js specific APIs */
} @@ js
)
// Conditional execution
suite("Integration Tests")(
test("database integration") {
/* Requires database */
} @@ ifEnv("DATABASE_URL", "test"),
test("debug mode features") {
/* Only in debug builds */
} @@ ifProp("debug", "true")
)Aspects for controlling how many times tests are executed.
/**
* Repeat test execution multiple times
* @param n number of repetitions
*/
def repeats(n: Int): TestAspectPoly
/**
* Configure number of samples for property-based tests
* @param n number of samples to generate and test
*/
def samples(n: Int): TestAspectPoly
/**
* Configure shrinking attempts for failed property tests
* @param n maximum shrinking attempts
*/
def shrinks(n: Int): TestAspectPoly
/**
* Set size parameter for generators
* @param n size value for Sized environment
*/
def size(n: Int): TestAspectPoly
/**
* Provide custom test configuration
* @param config test configuration parameters
*/
def config(config: TestConfig): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Repeat tests for reliability
test("flaky network operation") {
networkCall.map(result => assertTrue(result.isSuccess))
} @@ repeats(10)
// Property test configuration
test("sorting properties") {
check(listOf(anyInt)) { list =>
val sorted = list.sorted
assertTrue(sorted.size == list.size)
}
} @@ samples(1000) @@ shrinks(100)
// Size control for generators
test("large data structures") {
check(listOf(anyString)) { largeList =>
assertTrue(largeList.forall(_.nonEmpty))
}
} @@ size(10000)Aspects for controlling test output and logging behavior.
/**
* Suppress test output (silent execution)
*/
val silent: TestAspectPoly
/**
* Enable debug output with detailed information
*/
val debug: TestAspectPoly
/**
* Add custom annotations to test results
* @param annotations key-value annotations
*/
def annotate(annotations: TestAnnotation[_]*): TestAspectPoly
/**
* Tag tests with custom labels for filtering
* @param tags string tags for test organization
*/
def tag(tags: String*): TestAspectPoly
/**
* Execute with custom test logger
* @param logger custom logger implementation
*/
def withLogger(logger: TestLogger): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Silent execution for performance tests
suite("Performance Tests")(
test("benchmark operation") { /* performance test */ }
) @@ silent
// Tagged tests for organization
suite("API Tests")(
test("user endpoints") { /* test */ } @@ tag("user", "api"),
test("admin endpoints") { /* test */ } @@ tag("admin", "api", "security")
)
// Custom annotations
test("database test") {
/* test logic */
} @@ annotate(
TestAnnotation.database("postgresql"),
TestAnnotation.timeout(30.seconds)
)Combining multiple aspects for complex test behavior.
/**
* Compose aspects using the @@ operator
* Aspects are applied in order from left to right
*/
val composedAspect: TestAspectPoly =
timeout(30.seconds) @@
parallel @@
eventually @@
tag("integration")
/**
* Conditional aspect application
* @param condition when to apply the aspect
* @param aspect aspect to apply conditionally
*/
def when(condition: Boolean)(aspect: TestAspectPoly): TestAspectPolyUsage Examples:
import zio.test._
import zio.test.TestAspect._
// Complex aspect composition
suite("Complex Integration Suite")(
test("external service integration") { /* test */ },
test("database integration") { /* test */ },
test("file system integration") { /* test */ }
) @@ timeout(60.seconds) @@
parallel @@
eventually @@
tag("integration", "external") @@
ifEnv("RUN_INTEGRATION_TESTS", "true")
// Conditional aspects
val productionAspects =
when(sys.env.contains("PRODUCTION"))(timeout(10.seconds)) @@
when(sys.env.get("PARALLEL").contains("true"))(parallel) @@
tag("production")
suite("Production Tests")(
test("health check") { /* test */ }
) @@ productionAspectsCreating custom aspects for domain-specific testing needs.
/**
* Create custom test aspect from transformation function
* @param transform function that transforms test specs
* @return custom test aspect
*/
def custom[R, E](
transform: Spec[R, E] => Spec[R, E]
): TestAspect[Nothing, R, Nothing, E] =
new TestAspect[Nothing, R, Nothing, E] {
def apply[R1 >: Nothing <: R, E1 >: Nothing <: E](
spec: Spec[R1, E1]
): Spec[R1, E1] = transform(spec)
}
/**
* Create aspect that modifies test environment
* @param layer environment transformation layer
* @return aspect that provides the layer to tests
*/
def provideLayer[R, R1, E](
layer: ZLayer[R, E, R1]
): TestAspect[Nothing, R, Nothing, Any] =
new TestAspect[Nothing, R, Nothing, Any] {
def apply[R2 >: Nothing <: R, E2 >: Nothing <: Any](
spec: Spec[R2, E2]
): Spec[R2, E2] = spec.provideLayer(layer)
}Usage Examples:
import zio.test._
// Custom retry with logging
val retryWithLogging = TestAspect.custom[Any, Any] { spec =>
spec.mapTest { test =>
test.tapError(error =>
Console.printLine(s"Test failed, retrying: $error")
).retry(Schedule.recurs(2))
}
}
// Database transaction aspect
val inTransaction = TestAspect.provideLayer(
DatabaseTransactionLayer.test
)
// Usage
suite("Database Tests")(
test("user creation") { /* test */ },
test("user retrieval") { /* test */ }
) @@ inTransaction @@ retryWithLogging