0
# Test Aspects
1
2
Cross-cutting concerns for tests including timeouts, retries, conditional execution, and test organization. Test aspects allow you to modify test behavior without changing test logic.
3
4
## Capabilities
5
6
### Core Test Aspect Class
7
8
Base class for creating and composing test aspects.
9
10
```scala { .api }
11
/**
12
* TestAspect transforms test specifications by modifying their behavior.
13
* Aspects can change environment requirements, error types, or execution characteristics.
14
*/
15
abstract class TestAspect[+LowerR, -UpperR, +LowerE, -UpperE] {
16
/**
17
* Apply aspect to some tests in a spec based on predicate
18
* @param spec - Test specification to transform
19
* @returns Modified specification
20
*/
21
def some[R >: LowerR <: UpperR, E >: LowerE <: UpperE](spec: ZSpec[R, E]): ZSpec[R, E]
22
23
/**
24
* Apply aspect to all tests in a spec
25
* @param spec - Test specification to transform
26
* @returns Modified specification
27
*/
28
def all[R >: LowerR <: UpperR, E >: LowerE <: UpperE](spec: ZSpec[R, E]): ZSpec[R, E]
29
30
/**
31
* Compose this aspect with another aspect sequentially
32
* @param that - Aspect to compose with
33
* @returns Combined aspect
34
*/
35
def >>>[LowerR1 >: LowerR, UpperR1 <: UpperR, LowerE1 >: LowerE, UpperE1 <: UpperE](
36
that: TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
37
): TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
38
}
39
40
// Type aliases for common aspect patterns
41
type TestAspectPoly = TestAspect[Nothing, Any, Nothing, Any]
42
type TestAspectAtLeastR[R] = TestAspect[Nothing, R, Nothing, Any]
43
```
44
45
**Aspect Application Syntax:**
46
47
```scala { .api }
48
// Apply aspect to spec using @@ operator
49
spec @@ aspect
50
51
// Example usage
52
test("flaky test") {
53
assert(randomResult())(isTrue)
54
} @@ TestAspect.flaky @@ TestAspect.timeout(30.seconds)
55
```
56
57
### Basic Aspects
58
59
Fundamental aspects for test control.
60
61
```scala { .api }
62
/**
63
* Identity aspect that leaves tests unchanged
64
*/
65
val identity: TestAspectPoly
66
67
/**
68
* Ignore tests - marks them as ignored in results
69
*/
70
val ignore: TestAspectAtLeastR[Annotations]
71
```
72
73
**Usage Examples:**
74
75
```scala
76
// Temporarily ignore failing tests
77
test("broken test") {
78
assert(brokenFunction())(equalTo(42))
79
} @@ TestAspect.ignore
80
81
// Conditional ignoring
82
val aspectToUse = if (isCI) TestAspect.ignore else TestAspect.identity
83
test("local only test") {
84
// test logic
85
} @@ aspectToUse
86
```
87
88
### Lifecycle Aspects
89
90
Aspects for running effects before, after, or around tests.
91
92
```scala { .api }
93
/**
94
* Run effect before each test
95
* @param effect - Effect to run before test
96
* @returns Aspect that runs effect before each test
97
*/
98
def before[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any]
99
100
/**
101
* Run effect after each test
102
* @param effect - Effect to run after test
103
* @returns Aspect that runs effect after each test
104
*/
105
def after[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any]
106
107
/**
108
* Run effect before all tests in suite
109
* @param effect - Effect to run once before suite
110
* @returns Aspect that runs effect before suite
111
*/
112
def beforeAll[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any]
113
114
/**
115
* Run effect after all tests in suite
116
* @param effect - Effect to run once after suite
117
* @returns Aspect that runs effect after suite
118
*/
119
def afterAll[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any]
120
121
/**
122
* Run setup effect before and cleanup effect after each test
123
* @param before - Setup effect that produces resource
124
* @param after - Cleanup effect that consumes resource
125
* @returns Aspect that manages resource lifecycle
126
*/
127
def around[R0, E0, A](before: ZIO[R0, E0, A])(after: A => ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any]
128
129
/**
130
* Run setup effect before and cleanup effect after all tests
131
* @param before - Setup effect that produces resource
132
* @param after - Cleanup effect that consumes resource
133
* @returns Aspect that manages suite-level resource lifecycle
134
*/
135
def aroundAll[R0, E0, A](before: ZIO[R0, E0, A])(after: A => ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any]
136
```
137
138
**Usage Examples:**
139
140
```scala
141
// Database test setup/teardown
142
val dbAspect = TestAspect.before(setupDatabase) >>> TestAspect.after(cleanupDatabase)
143
144
suite("database tests")(
145
test("user creation") { /* test */ },
146
test("user deletion") { /* test */ }
147
) @@ dbAspect
148
149
// Resource management
150
val serverAspect = TestAspect.aroundAll(startTestServer) { server =>
151
server.shutdown()
152
}
153
154
// Per-test cleanup
155
test("file operations") {
156
// test that creates files
157
} @@ TestAspect.after(cleanupTempFiles)
158
```
159
160
### Timeout Aspects
161
162
Aspects for controlling test timeouts and timing warnings.
163
164
```scala { .api }
165
/**
166
* Timeout tests after specified duration
167
* @param duration - Maximum time to allow test to run
168
* @returns Aspect that fails tests exceeding timeout
169
*/
170
def timeout(duration: Duration): TestAspectAtLeastR[Live]
171
172
/**
173
* Show warning if test takes longer than specified duration
174
* @param duration - Duration after which to show warning
175
* @returns Aspect that warns about slow tests
176
*/
177
def timeoutWarning(duration: Duration): TestAspectAtLeastR[Live with Annotations]
178
```
179
180
**Usage Examples:**
181
182
```scala
183
// Fast tests should complete quickly
184
test("quick operation") {
185
assert(fastComputation())(equalTo(42))
186
} @@ TestAspect.timeout(100.millis)
187
188
// Warn about slow integration tests
189
suite("integration tests")(
190
testM("external API call") {
191
for {
192
result <- callExternalAPI()
193
} yield assert(result)(isSuccess)
194
}
195
) @@ TestAspect.timeoutWarning(5.seconds)
196
```
197
198
### Execution Control Aspects
199
200
Aspects for controlling how tests are executed.
201
202
```scala { .api }
203
/**
204
* Mark tests as flaky (may fail intermittently)
205
*/
206
val flaky: TestAspectAtLeastR[Annotations]
207
208
/**
209
* Mark tests as non-flaky (should not fail intermittently)
210
*/
211
val nonFlaky: TestAspectAtLeastR[Annotations]
212
213
/**
214
* Execute tests sequentially (disable parallelism)
215
*/
216
val sequential: TestAspectPoly
217
218
/**
219
* Execute tests in parallel (enable parallelism)
220
*/
221
val parallel: TestAspectPoly
222
223
/**
224
* Retry test until it succeeds within time limit
225
* @param duration - Maximum time to keep retrying
226
* @returns Aspect that retries failing tests
227
*/
228
def eventually(duration: Duration): TestAspectAtLeastR[Live with Annotations]
229
230
/**
231
* Retry test according to schedule on failure
232
* @param schedule - Retry schedule
233
* @returns Aspect that retries with custom schedule
234
*/
235
def retry(schedule: Schedule[Any, TestFailure[Any], Any]): TestAspectPoly
236
```
237
238
**Usage Examples:**
239
240
```scala
241
// Mark network tests as potentially flaky
242
test("network connectivity") {
243
assert(pingServer())(isTrue)
244
} @@ TestAspect.flaky
245
246
// Retry flaky external service calls
247
test("external service") {
248
assert(callExternalService())(isSuccess)
249
} @@ TestAspect.retry(Schedule.recurs(3) && Schedule.exponential(100.millis))
250
251
// Database tests should run sequentially
252
suite("database tests")(
253
test("create user") { /* test */ },
254
test("update user") { /* test */ }
255
) @@ TestAspect.sequential
256
```
257
258
### Conditional Aspects
259
260
Aspects that apply conditionally based on environment or platform.
261
262
```scala { .api }
263
/**
264
* Apply aspect only on JavaScript platform
265
* @param aspect - Aspect to apply on JS
266
* @returns Aspect that only applies on JavaScript
267
*/
268
def js(aspect: TestAspectPoly): TestAspectPoly
269
270
/**
271
* Apply aspect only on JVM platform
272
* @param aspect - Aspect to apply on JVM
273
* @returns Aspect that only applies on JVM
274
*/
275
def jvm(aspect: TestAspectPoly): TestAspectPoly
276
277
/**
278
* Apply aspect only on Native platform
279
* @param aspect - Aspect to apply on Native
280
* @returns Aspect that only applies on Native
281
*/
282
def native(aspect: TestAspectPoly): TestAspectPoly
283
284
/**
285
* Apply different aspects based on environment variable
286
* @param env - Environment variable name
287
* @param ifTrue - Aspect to apply if variable is set
288
* @param ifFalse - Aspect to apply if variable is not set
289
* @returns Conditional aspect
290
*/
291
def ifEnv(env: String)(ifTrue: TestAspectPoly, ifFalse: TestAspectPoly): TestAspectPoly
292
293
/**
294
* Apply different aspects based on system property
295
* @param prop - System property name
296
* @param ifTrue - Aspect to apply if property is set
297
* @param ifFalse - Aspect to apply if property is not set
298
* @returns Conditional aspect
299
*/
300
def ifProp(prop: String)(ifTrue: TestAspectPoly, ifFalse: TestAspectPoly): TestAspectPoly
301
```
302
303
**Usage Examples:**
304
305
```scala
306
// Platform-specific timeouts
307
test("file I/O performance") {
308
assert(performFileIO())(isLessThan(100.millis))
309
} @@ TestAspect.jvm(TestAspect.timeout(50.millis))
310
@@ TestAspect.js(TestAspect.timeout(200.millis))
311
312
// Environment-based test behavior
313
test("feature flag test") {
314
assert(newFeatureEnabled())(isTrue)
315
} @@ TestAspect.ifEnv("ENABLE_NEW_FEATURE")(
316
TestAspect.identity,
317
TestAspect.ignore
318
)
319
```
320
321
### Annotation Aspects
322
323
Aspects for adding metadata and organizing tests.
324
325
```scala { .api }
326
/**
327
* Tag tests with labels for filtering and organization
328
* @param tag - Primary tag
329
* @param tags - Additional tags
330
* @returns Aspect that adds tags to tests
331
*/
332
def tag(tag: String, tags: String*): TestAspectPoly
333
334
/**
335
* Alias for tag
336
*/
337
def tagged(tag: String, tags: String*): TestAspectPoly
338
339
/**
340
* Track fiber information for debugging
341
*/
342
val fibers: TestAspectAtLeastR[Annotations]
343
344
/**
345
* Add diagnostic information for debugging test issues
346
*/
347
val diagnose: TestAspectAtLeastR[TestConsole with Annotations]
348
```
349
350
**Usage Examples:**
351
352
```scala
353
// Organization with tags
354
test("user authentication") {
355
// test logic
356
} @@ TestAspect.tag("integration", "auth", "security")
357
358
// Debugging failing tests
359
test("complex concurrent operation") {
360
// complex test logic
361
} @@ TestAspect.diagnose @@ TestAspect.fibers
362
363
// Filter tests by tags in CI
364
// Run with: testOnly * -- --tags integration
365
suite("all tests")(
366
test("unit test") { /* test */ } @@ TestAspect.tag("unit"),
367
test("integration test") { /* test */ } @@ TestAspect.tag("integration")
368
)
369
```
370
371
### Sample Configuration Aspects
372
373
Aspects for controlling property-based test generation.
374
375
```scala { .api }
376
/**
377
* Set number of samples for property-based tests
378
* @param n - Number of samples to generate
379
* @returns Aspect that configures sample count
380
*/
381
def samples(n: Int): TestAspectAtLeastR[Annotations]
382
383
/**
384
* Set maximum number of shrinking attempts
385
* @param n - Maximum shrink attempts
386
* @returns Aspect that configures shrinking
387
*/
388
def shrinks(n: Int): TestAspectAtLeastR[Annotations]
389
```
390
391
**Usage Examples:**
392
393
```scala
394
// More thorough property testing
395
test("commutative property") {
396
check(Gen.anyInt, Gen.anyInt) { (a, b) =>
397
assert(a + b)(equalTo(b + a))
398
}
399
} @@ TestAspect.samples(1000)
400
401
// Disable shrinking for faster feedback
402
test("performance property") {
403
check(Gen.listOf(Gen.anyInt)) { list =>
404
assert(sort(list).length)(equalTo(list.length))
405
}
406
} @@ TestAspect.shrinks(0)
407
```
408
409
### Aspect Composition
410
411
Combining multiple aspects effectively.
412
413
```scala { .api }
414
// Sequential composition with >>>
415
val composedAspect =
416
TestAspect.timeout(30.seconds) >>>
417
TestAspect.retry(Schedule.recurs(2)) >>>
418
TestAspect.tag("integration")
419
420
// Multiple aspects on single test
421
test("complex test") {
422
// test logic
423
} @@ TestAspect.flaky
424
@@ TestAspect.timeout(10.seconds)
425
@@ TestAspect.tag("slow")
426
@@ TestAspect.samples(200)
427
```
428
429
**Common Aspect Patterns:**
430
431
```scala
432
// Database test aspect
433
val dbTestAspect =
434
TestAspect.before(initDB) >>>
435
TestAspect.after(cleanDB) >>>
436
TestAspect.sequential >>>
437
TestAspect.tag("database")
438
439
// Flaky network test aspect
440
val networkTestAspect =
441
TestAspect.flaky >>>
442
TestAspect.retry(Schedule.recurs(3)) >>>
443
TestAspect.timeout(30.seconds) >>>
444
TestAspect.tag("network", "integration")
445
446
// Performance test aspect
447
val perfTestAspect =
448
TestAspect.samples(1000) >>>
449
TestAspect.timeout(60.seconds) >>>
450
TestAspect.tag("performance")
451
```
452
453
## Types
454
455
### Core Aspect Types
456
457
```scala { .api }
458
/**
459
* Base test aspect class with environment and error constraints
460
*/
461
abstract class TestAspect[+LowerR, -UpperR, +LowerE, -UpperE]
462
463
/**
464
* Polymorphic test aspect with no environment or error constraints
465
*/
466
type TestAspectPoly = TestAspect[Nothing, Any, Nothing, Any]
467
468
/**
469
* Test aspect that requires at least environment R
470
*/
471
type TestAspectAtLeastR[R] = TestAspect[Nothing, R, Nothing, Any]
472
```
473
474
### Per-Test Aspects
475
476
```scala { .api }
477
/**
478
* Base class for aspects that transform individual tests
479
*/
480
abstract class PerTest[+LowerR, -UpperR, +LowerE, -UpperE] extends TestAspect[LowerR, UpperR, LowerE, UpperE] {
481
/**
482
* Transform a single test
483
* @param test - Test to transform
484
* @returns Transformed test
485
*/
486
def perTest[R <: UpperR, E >: LowerE](test: ZIO[R, TestFailure[E], TestSuccess]): ZIO[R, TestFailure[E], TestSuccess]
487
}
488
```
489
490
### Annotation Types
491
492
```scala { .api }
493
/**
494
* Test annotation for adding metadata
495
*/
496
trait TestAnnotation[V] {
497
def identifier: String
498
def initial: V
499
def combine(v1: V, v2: V): V
500
}
501
502
/**
503
* Map of test annotations
504
*/
505
final case class TestAnnotationMap(
506
annotations: Map[TestAnnotation[Any], Any]
507
) {
508
def annotate[V](key: TestAnnotation[V], value: V): TestAnnotationMap
509
def get[V](key: TestAnnotation[V]): V
510
}
511
```