0
# ZIO Test Framework
1
2
ZIO Test provides a comprehensive testing framework with property-based testing, test services, and seamless integration with the ZIO effect system for testing concurrent and async code.
3
4
## Capabilities
5
6
### Test Specifications
7
8
Define test suites and individual test cases with full ZIO effect support and dependency injection.
9
10
```scala { .api }
11
/**
12
* A test specification defines a suite of tests
13
*/
14
sealed trait Spec[-R, +E] {
15
/** Transform the environment type */
16
def provideLayer[E1 >: E, R0](layer: ZLayer[R0, E1, R]): Spec[R0, E1]
17
18
/** Add test aspects to modify test behavior */
19
def @@[R1 <: R](aspect: TestAspect[R1, E]): Spec[R1, E]
20
21
/** Map over the error type */
22
def mapError[E2](f: E => E2): Spec[R, E2]
23
}
24
25
/**
26
* Base class for ZIO test suites
27
*/
28
abstract class ZIOSpec[R] extends ZIOApp {
29
/** Define the test specification */
30
def spec: Spec[R, Any]
31
32
/** Provide dependencies for tests */
33
def bootstrap: ZLayer[Any, Any, R] = ZLayer.empty
34
}
35
36
/**
37
* Default test suite with no special requirements
38
*/
39
abstract class ZIOSpecDefault extends ZIOSpec[Any] {
40
final def bootstrap: ZLayer[Any, Any, Any] = ZLayer.empty
41
}
42
43
/**
44
* Test construction methods
45
*/
46
object ZIOSpecDefault {
47
/** Create a simple test */
48
def test(label: String)(assertion: => TestResult): Spec[Any, Nothing]
49
50
/** Create a test with full ZIO effects */
51
def testM[R, E](label: String)(assertion: ZIO[R, E, TestResult]): Spec[R, E]
52
53
/** Create a test suite */
54
def suite(label: String)(specs: Spec[Any, Any]*): Spec[Any, Any]
55
56
/** Create a test suite with environment requirements */
57
def suiteM[R, E](label: String)(specs: ZIO[R, E, Spec[R, E]]): Spec[R, E]
58
}
59
```
60
61
**Usage Examples:**
62
63
```scala
64
import zio._
65
import zio.test._
66
67
object MyServiceSpec extends ZIOSpecDefault {
68
def spec = suite("MyService")(
69
test("should handle simple operations") {
70
val result = 2 + 2
71
assertTrue(result == 4)
72
},
73
74
test("should work with ZIO effects") {
75
for {
76
result <- ZIO.succeed(42)
77
} yield assertTrue(result > 0)
78
},
79
80
suite("error handling")(
81
test("should catch failures") {
82
val failing = ZIO.fail("error")
83
assertZIO(failing.flip)(equalTo("error"))
84
}
85
)
86
)
87
}
88
89
// With dependency injection
90
object DatabaseSpec extends ZIOSpec[Database] {
91
def spec = suite("Database")(
92
test("should save and retrieve users") {
93
for {
94
db <- ZIO.service[Database]
95
user <- db.save(User("alice", "alice@example.com"))
96
retrieved <- db.findById(user.id)
97
} yield assertTrue(retrieved.contains(user))
98
}
99
)
100
101
def bootstrap = DatabaseLive.layer
102
}
103
```
104
105
### Assertions
106
107
Comprehensive assertion library for testing values, effects, and complex conditions.
108
109
```scala { .api }
110
/**
111
* Represents the result of a single test
112
*/
113
sealed trait TestResult {
114
/** Combine with another test result using logical AND */
115
def &&(that: => TestResult): TestResult
116
117
/** Combine with another test result using logical OR */
118
def ||(that: => TestResult): TestResult
119
120
/** Negate the test result */
121
def unary_! : TestResult
122
123
/** Add a custom label to the result */
124
def label(label: String): TestResult
125
}
126
127
/**
128
* Type-safe assertions for testing
129
*/
130
sealed trait Assertion[-A] {
131
/** Test the assertion against a value */
132
def test(a: A): TestResult
133
134
/** Combine with another assertion using logical AND */
135
def &&[A1 <: A](that: Assertion[A1]): Assertion[A1]
136
137
/** Combine with another assertion using logical OR */
138
def ||[A1 <: A](that: Assertion[A1]): Assertion[A1]
139
140
/** Negate the assertion */
141
def unary_! : Assertion[A]
142
}
143
144
/**
145
* Common assertion constructors
146
*/
147
object Assertion {
148
/** Assert equality */
149
def equalTo[A](expected: A): Assertion[A]
150
151
/** Assert approximate equality for numbers */
152
def approximatelyEquals(expected: Double, tolerance: Double): Assertion[Double]
153
154
/** Assert value is true */
155
val isTrue: Assertion[Boolean]
156
157
/** Assert value is false */
158
val isFalse: Assertion[Boolean]
159
160
/** Assert value is empty (for collections) */
161
val isEmpty: Assertion[Iterable[Any]]
162
163
/** Assert value is non-empty */
164
val isNonEmpty: Assertion[Iterable[Any]]
165
166
/** Assert collection contains element */
167
def contains[A](element: A): Assertion[Iterable[A]]
168
169
/** Assert string contains substring */
170
def containsString(substring: String): Assertion[String]
171
172
/** Assert string starts with prefix */
173
def startsWith(prefix: String): Assertion[String]
174
175
/** Assert string ends with suffix */
176
def endsWith(suffix: String): Assertion[String]
177
178
/** Assert string matches regex */
179
def matchesRegex(regex: String): Assertion[String]
180
181
/** Assert value is greater than */
182
def isGreaterThan[A](expected: A)(implicit ord: Ordering[A]): Assertion[A]
183
184
/** Assert value is less than */
185
def isLessThan[A](expected: A)(implicit ord: Ordering[A]): Assertion[A]
186
187
/** Assert value is within range */
188
def isWithin[A](min: A, max: A)(implicit ord: Ordering[A]): Assertion[A]
189
190
/** Assert all elements satisfy condition */
191
def forall[A](assertion: Assertion[A]): Assertion[Iterable[A]]
192
193
/** Assert at least one element satisfies condition */
194
def exists[A](assertion: Assertion[A]): Assertion[Iterable[A]]
195
196
/** Assert collection has specific size */
197
def hasSize[A](expected: Int): Assertion[Iterable[A]]
198
}
199
200
/**
201
* Convenient assertion methods
202
*/
203
def assertTrue(condition: => Boolean): TestResult
204
def assert[A](value: A)(assertion: Assertion[A]): TestResult
205
def assertZIO[R, E, A](effect: ZIO[R, E, A])(assertion: Assertion[A]): ZIO[R, E, TestResult]
206
```
207
208
**Usage Examples:**
209
210
```scala
211
// Basic assertions
212
test("basic assertions") {
213
assertTrue(2 + 2 == 4) &&
214
assert("hello world")(containsString("world")) &&
215
assert(List(1, 2, 3))(hasSize(3) && contains(2))
216
}
217
218
// Effect assertions
219
test("effect assertions") {
220
val computation = ZIO.succeed(42)
221
assertZIO(computation)(isGreaterThan(40))
222
}
223
224
// Complex conditions
225
test("complex conditions") {
226
val data = List("apple", "banana", "cherry")
227
assert(data)(
228
hasSize(3) &&
229
forall(startsWith("a") || startsWith("b") || startsWith("c")) &&
230
exists(containsString("nan"))
231
)
232
}
233
234
// Custom labels
235
test("with custom labels") {
236
val result = complexCalculation()
237
assert(result)(isGreaterThan(0)).label("result should be positive") &&
238
assert(result)(isLessThan(100)).label("result should be reasonable")
239
}
240
```
241
242
### Property-Based Testing
243
244
Generate random test data and verify properties hold across many test cases.
245
246
```scala { .api }
247
/**
248
* Generator for producing random test data
249
*/
250
sealed trait Gen[+R, +A] {
251
/** Transform generated values */
252
def map[B](f: A => B): Gen[R, B]
253
254
/** Chain generators together */
255
def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]
256
257
/** Filter generated values */
258
def filter(f: A => Boolean): Gen[R, A]
259
260
/** Generate optional values */
261
def optional: Gen[R, Option[A]]
262
263
/** Generate lists of values */
264
def list: Gen[R, List[A]]
265
266
/** Generate lists of specific size */
267
def listOfN(n: Int): Gen[R, List[A]]
268
269
/** Generate values between bounds */
270
def bounded(min: A, max: A)(implicit ord: Ordering[A]): Gen[R, A]
271
}
272
273
/**
274
* Common generators
275
*/
276
object Gen {
277
/** Generate random integers */
278
val int: Gen[Any, Int]
279
280
/** Generate integers in range */
281
def int(min: Int, max: Int): Gen[Any, Int]
282
283
/** Generate random strings */
284
val string: Gen[Any, String]
285
286
/** Generate alphanumeric strings */
287
val alphaNumericString: Gen[Any, String]
288
289
/** Generate strings of specific length */
290
def stringN(n: Int): Gen[Any, String]
291
292
/** Generate random booleans */
293
val boolean: Gen[Any, Boolean]
294
295
/** Generate random doubles */
296
val double: Gen[Any, Double]
297
298
/** Generate from a list of options */
299
def oneOf[A](as: A*): Gen[Any, A]
300
301
/** Generate from weighted options */
302
def weighted[A](weightedValues: (A, Double)*): Gen[Any, A]
303
304
/** Generate constant values */
305
def const[A](a: A): Gen[Any, A]
306
307
/** Generate collections */
308
def listOf[R, A](gen: Gen[R, A]): Gen[R, List[A]]
309
def setOf[R, A](gen: Gen[R, A]): Gen[R, Set[A]]
310
def mapOf[R, K, V](keyGen: Gen[R, K], valueGen: Gen[R, V]): Gen[R, Map[K, V]]
311
312
/** Generate case classes */
313
def zip[R, A, B](genA: Gen[R, A], genB: Gen[R, B]): Gen[R, (A, B)]
314
def zip3[R, A, B, C](genA: Gen[R, A], genB: Gen[R, B], genC: Gen[R, C]): Gen[R, (A, B, C)]
315
}
316
317
/**
318
* Property-based test construction
319
*/
320
def check[R, A](gen: Gen[R, A])(test: A => TestResult): ZIO[R, Nothing, TestResult]
321
def checkAll[R, A](gen: Gen[R, A])(test: A => TestResult): ZIO[R, Nothing, TestResult]
322
def checkN(n: Int): CheckN
323
```
324
325
**Usage Examples:**
326
327
```scala
328
import zio.test.Gen._
329
330
// Simple property test
331
test("string reverse property") {
332
check(string) { s =>
333
assertTrue(s.reverse.reverse == s)
334
}
335
}
336
337
// Multiple generators
338
test("addition is commutative") {
339
check(int, int) { (a, b) =>
340
assertTrue(a + b == b + a)
341
}
342
}
343
344
// Custom generators
345
val positiveInt = int(1, 1000)
346
val email = for {
347
name <- alphaNumericString
348
domain <- oneOf("gmail.com", "yahoo.com", "example.org")
349
} yield s"$name@$domain"
350
351
test("email validation") {
352
check(email) { email =>
353
assertTrue(email.contains("@") && email.contains("."))
354
}
355
}
356
357
// Complex data structures
358
case class User(name: String, age: Int, email: String)
359
360
val userGen = for {
361
name <- alphaNumericString
362
age <- int(18, 100)
363
email <- email
364
} yield User(name, age, email)
365
366
test("user serialization") {
367
check(userGen) { user =>
368
val serialized = serialize(user)
369
val deserialized = deserialize(serialized)
370
assertTrue(deserialized == user)
371
}
372
}
373
```
374
375
### Test Services
376
377
Mock implementations of ZIO services for deterministic testing without external dependencies.
378
379
```scala { .api }
380
/**
381
* Test implementation of Clock service for deterministic time testing
382
*/
383
trait TestClock extends Clock {
384
/** Adjust the test clock by a duration */
385
def adjust(duration: Duration): UIO[Unit]
386
387
/** Set the test clock to a specific instant */
388
def setTime(instant: Instant): UIO[Unit]
389
390
/** Get current test time */
391
def instant: UIO[Instant]
392
393
/** Get time zone */
394
def timeZone: UIO[ZoneId]
395
}
396
397
/**
398
* Test implementation of Console for capturing output
399
*/
400
trait TestConsole extends Console {
401
/** Get all output written to stdout */
402
def output: UIO[Vector[String]]
403
404
/** Get all output written to stderr */
405
def errorOutput: UIO[Vector[String]]
406
407
/** Clear all captured output */
408
def clearOutput: UIO[Unit]
409
410
/** Feed input to be read by readLine */
411
def feedLines(lines: String*): UIO[Unit]
412
}
413
414
/**
415
* Test implementation of Random for predictable randomness
416
*/
417
trait TestRandom extends Random {
418
/** Set the random seed */
419
def setSeed(seed: Long): UIO[Unit]
420
421
/** Feed specific random values */
422
def feedInts(ints: Int*): UIO[Unit]
423
def feedDoubles(doubles: Double*): UIO[Unit]
424
def feedBooleans(booleans: Boolean*): UIO[Unit]
425
}
426
427
/**
428
* Test implementation of System for mocking environment
429
*/
430
trait TestSystem extends System {
431
/** Set environment variables */
432
def putEnv(name: String, value: String): UIO[Unit]
433
434
/** Set system properties */
435
def putProperty(name: String, value: String): UIO[Unit]
436
437
/** Clear environment variables */
438
def clearEnv(name: String): UIO[Unit]
439
440
/** Clear system properties */
441
def clearProperty(name: String): UIO[Unit]
442
}
443
```
444
445
**Usage Examples:**
446
447
```scala
448
// Testing with TestClock
449
test("timeout behavior") {
450
for {
451
fiber <- longRunningTask.timeout(5.seconds).fork
452
_ <- TestClock.adjust(6.seconds)
453
result <- fiber.join
454
} yield assertTrue(result.isEmpty)
455
}
456
457
// Testing with TestConsole
458
test("console output") {
459
for {
460
_ <- Console.printLine("Hello")
461
_ <- Console.printLine("World")
462
output <- TestConsole.output
463
} yield assertTrue(output == Vector("Hello", "World"))
464
}
465
466
// Testing with TestRandom
467
test("random behavior") {
468
for {
469
_ <- TestRandom.feedInts(1, 2, 3)
470
first <- Random.nextInt
471
second <- Random.nextInt
472
third <- Random.nextInt
473
} yield assertTrue(first == 1 && second == 2 && third == 3)
474
}
475
476
// Combined service testing
477
test("application behavior") {
478
val program = for {
479
config <- ZIO.service[AppConfig]
480
_ <- Console.printLine(s"Starting on port ${config.port}")
481
_ <- Clock.sleep(1.second)
482
_ <- Console.printLine("Started successfully")
483
} yield ()
484
485
program.provide(
486
TestConsole.layer,
487
TestClock.layer,
488
ZLayer.succeed(AppConfig(8080))
489
) *>
490
for {
491
_ <- TestClock.adjust(1.second)
492
output <- TestConsole.output
493
} yield assertTrue(
494
output.contains("Starting on port 8080") &&
495
output.contains("Started successfully")
496
)
497
}
498
```
499
500
### Test Aspects
501
502
Modify test behavior with reusable aspects for timeouts, retries, parallelism, and more.
503
504
```scala { .api }
505
/**
506
* Test aspects modify how tests are executed
507
*/
508
sealed trait TestAspect[-R, +E] {
509
/** Combine with another aspect */
510
def @@[R1 <: R](that: TestAspect[R1, E]): TestAspect[R1, E]
511
}
512
513
/**
514
* Common test aspects
515
*/
516
object TestAspect {
517
/** Set timeout for tests */
518
def timeout(duration: Duration): TestAspect[Any, Nothing]
519
520
/** Retry failed tests */
521
def retry(n: Int): TestAspect[Any, Nothing]
522
523
/** Run tests eventually (keep retrying until success) */
524
val eventually: TestAspect[Any, Nothing]
525
526
/** Run tests in parallel */
527
val parallel: TestAspect[Any, Nothing]
528
529
/** Run tests sequentially */
530
val sequential: TestAspect[Any, Nothing]
531
532
/** Ignore/skip tests */
533
val ignore: TestAspect[Any, Nothing]
534
535
/** Run only on specific platforms */
536
def jvmOnly: TestAspect[Any, Nothing]
537
def jsOnly: TestAspect[Any, Nothing]
538
def nativeOnly: TestAspect[Any, Nothing]
539
540
/** Repeat tests multiple times */
541
def repeats(n: Int): TestAspect[Any, Nothing]
542
543
/** Add samples for property-based tests */
544
def samples(n: Int): TestAspect[Any, Nothing]
545
546
/** Shrink failing test cases */
547
val shrinks: TestAspect[Any, Nothing]
548
549
/** Run with specific test data */
550
def withLiveClock: TestAspect[Any, Nothing]
551
def withLiveConsole: TestAspect[Any, Nothing]
552
def withLiveRandom: TestAspect[Any, Nothing]
553
}
554
```
555
556
**Usage Examples:**
557
558
```scala
559
// Timeout aspect
560
test("long running operation") {
561
heavyComputation
562
} @@ TestAspect.timeout(30.seconds)
563
564
// Retry flaky tests
565
test("flaky network operation") {
566
networkCall
567
} @@ TestAspect.retry(3)
568
569
// Eventually succeeding tests
570
test("eventual consistency") {
571
checkEventualConsistency
572
} @@ TestAspect.eventually
573
574
// Platform-specific tests
575
test("JVM-specific functionality") {
576
jvmSpecificCode
577
} @@ TestAspect.jvmOnly
578
579
// Property test configuration
580
test("complex property") {
581
check(complexGen)(complexProperty)
582
} @@ TestAspect.samples(1000) @@ TestAspect.shrinks
583
584
// Suite-level aspects
585
suite("integration tests")(
586
test("database operations") { ... },
587
test("api calls") { ... }
588
) @@ TestAspect.sequential @@ TestAspect.timeout(5.minutes)
589
```