0
# Test Services
1
2
Deterministic test services that replace system services during testing for predictable, reproducible test execution.
3
4
## Capabilities
5
6
### TestClock
7
8
Deterministic clock service for controlling time progression in tests.
9
10
```scala { .api }
11
/**
12
* Test clock that allows manual time control
13
*/
14
trait TestClock extends Clock {
15
/**
16
* Advance the clock by the specified duration
17
* @param duration amount to advance time
18
* @return effect that advances the clock
19
*/
20
def adjust(duration: Duration): UIO[Unit]
21
22
/**
23
* Set the clock to an absolute time
24
* @param duration absolute time from epoch
25
* @return effect that sets the clock time
26
*/
27
def setTime(duration: Duration): UIO[Unit]
28
29
/**
30
* Get the current time zone
31
* @return current time zone
32
*/
33
def timeZone: UIO[ZoneId]
34
35
/**
36
* Set the time zone for the clock
37
* @param zone new time zone
38
* @return effect that sets the time zone
39
*/
40
def setTimeZone(zone: ZoneId): UIO[Unit]
41
42
/**
43
* Sleep for the specified duration (uses test time)
44
* @param duration duration to sleep
45
* @return effect that completes after the duration
46
*/
47
override def sleep(duration: Duration): UIO[Unit]
48
49
/**
50
* Get current time in specified time unit
51
* @param unit time unit for the result
52
* @return current time in the specified unit
53
*/
54
override def currentTime(unit: TimeUnit): UIO[Long]
55
56
/**
57
* Get current time as instant
58
* @return current time instant
59
*/
60
override def instant: UIO[Instant]
61
}
62
63
object TestClock {
64
/**
65
* Default test clock implementation starting at epoch
66
*/
67
val default: ULayer[TestClock]
68
69
/**
70
* Create test clock starting at specified time
71
* @param startTime initial clock time
72
* @return layer providing test clock
73
*/
74
def live(startTime: Instant): ULayer[TestClock]
75
}
76
```
77
78
**Usage Examples:**
79
80
```scala
81
import zio.test._
82
import zio._
83
import java.time.Instant
84
import java.util.concurrent.TimeUnit
85
86
// Basic time manipulation
87
test("time-dependent operation") {
88
for {
89
clock <- ZIO.service[TestClock]
90
start <- clock.currentTime(TimeUnit.MILLISECONDS)
91
_ <- clock.adjust(1.hour)
92
end <- clock.currentTime(TimeUnit.MILLISECONDS)
93
} yield assertTrue(end - start == 3600000)
94
}
95
96
// Testing scheduled operations
97
test("scheduled task execution") {
98
for {
99
clock <- ZIO.service[TestClock]
100
ref <- Ref.make(0)
101
// Schedule task to run every minute
102
fiber <- (ZIO.sleep(1.minute) *> ref.update(_ + 1)).forever.fork
103
// Advance time and check execution
104
_ <- clock.adjust(5.minutes)
105
count <- ref.get
106
_ <- fiber.interrupt
107
} yield assertTrue(count == 5)
108
}
109
110
// Testing timeout behavior
111
test("operation timeout") {
112
for {
113
clock <- ZIO.service[TestClock]
114
fiber <- (ZIO.sleep(10.seconds) *> ZIO.succeed("completed")).fork
115
_ <- clock.adjust(5.seconds)
116
result <- fiber.poll
117
} yield assertTrue(result.isEmpty) // Should still be running
118
}
119
```
120
121
### TestConsole
122
123
Console service for testing input/output operations with configurable responses.
124
125
```scala { .api }
126
/**
127
* Test console that captures output and provides controllable input
128
*/
129
trait TestConsole extends Console {
130
/**
131
* Get all captured output lines
132
* @return vector of output lines
133
*/
134
def output: UIO[Vector[String]]
135
136
/**
137
* Clear all captured output
138
* @return effect that clears output buffer
139
*/
140
def clearOutput: UIO[Unit]
141
142
/**
143
* Provide input lines for console reading
144
* @param lines input lines to feed to console
145
* @return effect that feeds the lines
146
*/
147
def feedLines(lines: String*): UIO[Unit]
148
149
/**
150
* Get all captured error output
151
* @return vector of error output lines
152
*/
153
def errorOutput: UIO[Vector[String]]
154
155
/**
156
* Clear all captured error output
157
* @return effect that clears error output buffer
158
*/
159
def clearErrorOutput: UIO[Unit]
160
161
/**
162
* Print line to output (captured)
163
* @param line line to print
164
* @return effect that prints the line
165
*/
166
override def printLine(line: Any): IO[IOException, Unit]
167
168
/**
169
* Print to output without newline (captured)
170
* @param text text to print
171
* @return effect that prints the text
172
*/
173
override def print(text: Any): IO[IOException, Unit]
174
175
/**
176
* Read line from input (uses fed lines)
177
* @return effect that reads a line
178
*/
179
override def readLine: IO[IOException, String]
180
}
181
182
object TestConsole {
183
/**
184
* Debug console that prints to real console and captures
185
*/
186
val debug: ULayer[TestConsole]
187
188
/**
189
* Silent console that only captures without real output
190
*/
191
val silent: ULayer[TestConsole]
192
}
193
```
194
195
**Usage Examples:**
196
197
```scala
198
import zio.test._
199
import zio._
200
import java.io.IOException
201
202
// Capture and verify output
203
test("application output") {
204
for {
205
_ <- Console.printLine("Hello, World!")
206
_ <- Console.printLine("ZIO Test is awesome!")
207
console <- ZIO.service[TestConsole]
208
output <- console.output
209
} yield assertTrue(
210
output == Vector("Hello, World!", "ZIO Test is awesome!")
211
)
212
}
213
214
// Test interactive console programs
215
test("interactive program") {
216
def interactiveProgram: ZIO[Console, IOException, String] =
217
for {
218
_ <- Console.printLine("What's your name?")
219
name <- Console.readLine
220
_ <- Console.printLine(s"Hello, $name!")
221
} yield name
222
223
for {
224
console <- ZIO.service[TestConsole]
225
_ <- console.feedLines("Alice")
226
result <- interactiveProgram
227
output <- console.output
228
} yield assertTrue(
229
result == "Alice" &&
230
output == Vector("What's your name?", "Hello, Alice!")
231
)
232
}
233
234
// Test error output
235
test("error logging") {
236
def logError(message: String): ZIO[Console, IOException, Unit] =
237
Console.printLineError(s"ERROR: $message")
238
239
for {
240
_ <- logError("Something went wrong")
241
console <- ZIO.service[TestConsole]
242
errorOutput <- console.errorOutput
243
} yield assertTrue(errorOutput == Vector("ERROR: Something went wrong"))
244
}
245
```
246
247
### TestRandom
248
249
Deterministic random service for reproducible random number generation in tests.
250
251
```scala { .api }
252
/**
253
* Test random service with controllable seed and predefined values
254
*/
255
trait TestRandom extends Random {
256
/**
257
* Set the random seed for reproducible generation
258
* @param seed random seed value
259
* @return effect that sets the seed
260
*/
261
def setSeed(seed: Long): UIO[Unit]
262
263
/**
264
* Feed predefined boolean values
265
* @param booleans boolean values to return from nextBoolean
266
* @return effect that feeds the values
267
*/
268
def feedBooleans(booleans: Boolean*): UIO[Unit]
269
270
/**
271
* Clear all fed boolean values
272
* @return effect that clears boolean buffer
273
*/
274
def clearBooleans: UIO[Unit]
275
276
/**
277
* Feed predefined integer values
278
* @param ints integer values to return from nextInt
279
* @return effect that feeds the values
280
*/
281
def feedInts(ints: Int*): UIO[Unit]
282
283
/**
284
* Clear all fed integer values
285
* @return effect that clears integer buffer
286
*/
287
def clearInts: UIO[Unit]
288
289
/**
290
* Feed predefined long values
291
* @param longs long values to return from nextLong
292
* @return effect that feeds the values
293
*/
294
def feedLongs(longs: Long*): UIO[Unit]
295
296
/**
297
* Clear all fed long values
298
* @return effect that clears long buffer
299
*/
300
def clearLongs: UIO[Unit]
301
302
/**
303
* Feed predefined double values
304
* @param doubles double values to return from nextDouble
305
* @return effect that feeds the values
306
*/
307
def feedDoubles(doubles: Double*): UIO[Unit]
308
309
/**
310
* Clear all fed double values
311
* @return effect that clears double buffer
312
*/
313
def clearDoubles: UIO[Unit]
314
315
/**
316
* Generate next boolean (uses fed values or seed-based generation)
317
* @return effect producing boolean value
318
*/
319
override def nextBoolean: UIO[Boolean]
320
321
/**
322
* Generate next integer in range (uses fed values or seed-based generation)
323
* @param n upper bound (exclusive)
324
* @return effect producing integer value
325
*/
326
override def nextInt(n: Int): UIO[Int]
327
328
/**
329
* Generate next long (uses fed values or seed-based generation)
330
* @return effect producing long value
331
*/
332
override def nextLong: UIO[Long]
333
334
/**
335
* Generate next double (uses fed values or seed-based generation)
336
* @return effect producing double value
337
*/
338
override def nextDouble: UIO[Double]
339
}
340
341
object TestRandom {
342
/**
343
* Deterministic random with fixed seed
344
* @param seed initial seed value
345
* @return layer providing test random
346
*/
347
def deterministic(seed: Long): ULayer[TestRandom]
348
349
/**
350
* Deterministic random with default seed (0)
351
*/
352
val deterministic: ULayer[TestRandom]
353
}
354
```
355
356
**Usage Examples:**
357
358
```scala
359
import zio.test._
360
import zio._
361
362
// Reproducible random testing
363
test("random behavior with fixed seed") {
364
def randomOperation: ZIO[Random, Nothing, List[Int]] =
365
ZIO.collectAll(List.fill(5)(Random.nextInt(100)))
366
367
for {
368
random <- ZIO.service[TestRandom]
369
_ <- random.setSeed(12345L)
370
result1 <- randomOperation
371
_ <- random.setSeed(12345L)
372
result2 <- randomOperation
373
} yield assertTrue(result1 == result2) // Same seed = same results
374
}
375
376
// Control random values explicitly
377
test("specific random sequence") {
378
def coinFlips(n: Int): ZIO[Random, Nothing, List[String]] =
379
ZIO.collectAll(
380
List.fill(n)(Random.nextBoolean.map(if (_) "heads" else "tails"))
381
)
382
383
for {
384
random <- ZIO.service[TestRandom]
385
_ <- random.feedBooleans(true, false, true, true, false)
386
flips <- coinFlips(5)
387
} yield assertTrue(
388
flips == List("heads", "tails", "heads", "heads", "tails")
389
)
390
}
391
392
// Test random distributions
393
test("random distribution properties") {
394
def generateSample: ZIO[Random, Nothing, List[Int]] =
395
ZIO.collectAll(List.fill(1000)(Random.nextInt(100)))
396
397
for {
398
random <- ZIO.service[TestRandom]
399
_ <- random.setSeed(42L)
400
sample <- generateSample
401
average = sample.sum.toDouble / sample.size
402
} yield assertTrue(average >= 40.0 && average <= 60.0) // Should be around 50
403
}
404
```
405
406
### TestSystem
407
408
System service for testing environment variables and system properties.
409
410
```scala { .api }
411
/**
412
* Test system service with controllable environment and properties
413
*/
414
trait TestSystem extends System {
415
/**
416
* Set environment variable for testing
417
* @param name variable name
418
* @param value variable value
419
* @return effect that sets the variable
420
*/
421
def putEnv(name: String, value: String): UIO[Unit]
422
423
/**
424
* Clear environment variable
425
* @param name variable name to clear
426
* @return effect that clears the variable
427
*/
428
def clearEnv(name: String): UIO[Unit]
429
430
/**
431
* Set system property for testing
432
* @param name property name
433
* @param value property value
434
* @return effect that sets the property
435
*/
436
def putProperty(name: String, value: String): UIO[Unit]
437
438
/**
439
* Clear system property
440
* @param name property name to clear
441
* @return effect that clears the property
442
*/
443
def clearProperty(name: String): UIO[Unit]
444
445
/**
446
* Get environment variable
447
* @param name variable name
448
* @return effect producing optional variable value
449
*/
450
override def env(name: String): IO[SecurityException, Option[String]]
451
452
/**
453
* Get system property
454
* @param name property name
455
* @return effect producing optional property value
456
*/
457
override def property(name: String): IO[Throwable, Option[String]]
458
459
/**
460
* Get line separator for current system
461
* @return line separator string
462
*/
463
override def lineSeparator: UIO[String]
464
}
465
466
object TestSystem {
467
/**
468
* Default test system implementation
469
*/
470
val default: ULayer[TestSystem]
471
472
/**
473
* Test system with initial environment and properties
474
* @param env initial environment variables
475
* @param props initial system properties
476
* @return layer providing test system
477
*/
478
def live(
479
env: Map[String, String] = Map.empty,
480
props: Map[String, String] = Map.empty
481
): ULayer[TestSystem]
482
}
483
```
484
485
**Usage Examples:**
486
487
```scala
488
import zio.test._
489
import zio._
490
491
// Test environment-dependent behavior
492
test("application configuration from environment") {
493
def getConfig: ZIO[System, SecurityException, AppConfig] =
494
for {
495
host <- System.env("DB_HOST").map(_.getOrElse("localhost"))
496
port <- System.env("DB_PORT").map(_.getOrElse("5432"))
497
} yield AppConfig(host, port.toInt)
498
499
for {
500
system <- ZIO.service[TestSystem]
501
_ <- system.putEnv("DB_HOST", "test-db.example.com")
502
_ <- system.putEnv("DB_PORT", "3306")
503
config <- getConfig
504
} yield assertTrue(
505
config.host == "test-db.example.com" &&
506
config.port == 3306
507
)
508
}
509
510
// Test system property behavior
511
test("debug mode from system property") {
512
def isDebugMode: ZIO[System, Throwable, Boolean] =
513
System.property("debug").map(_.contains("true"))
514
515
for {
516
system <- ZIO.service[TestSystem]
517
_ <- system.putProperty("debug", "true")
518
debug1 <- isDebugMode
519
_ <- system.clearProperty("debug")
520
debug2 <- isDebugMode
521
} yield assertTrue(debug1 && !debug2)
522
}
523
524
// Test cross-platform line separator handling
525
test("text file generation") {
526
def generateTextFile(lines: List[String]): ZIO[System, Nothing, String] =
527
for {
528
separator <- System.lineSeparator
529
} yield lines.mkString(separator)
530
531
for {
532
result <- generateTextFile(List("line1", "line2", "line3"))
533
} yield assertTrue(result.contains("line1") && result.contains("line2"))
534
}
535
536
case class AppConfig(host: String, port: Int)
537
```
538
539
### Service Access Functions
540
541
Convenience functions for accessing test services from the test environment.
542
543
```scala { .api }
544
/**
545
* Access TestClock service
546
* @return effect producing TestClock instance
547
*/
548
def testClock: URIO[TestClock, TestClock]
549
550
/**
551
* Access TestClock and apply function
552
* @param f function to apply to TestClock
553
* @return effect with TestClock applied to function
554
*/
555
def testClockWith[R, E, A](f: TestClock => ZIO[R, E, A]): ZIO[R with TestClock, E, A]
556
557
/**
558
* Access TestConsole service
559
* @return effect producing TestConsole instance
560
*/
561
def testConsole: URIO[TestConsole, TestConsole]
562
563
/**
564
* Access TestConsole and apply function
565
* @param f function to apply to TestConsole
566
* @return effect with TestConsole applied to function
567
*/
568
def testConsoleWith[R, E, A](f: TestConsole => ZIO[R, E, A]): ZIO[R with TestConsole, E, A]
569
570
/**
571
* Access TestRandom service
572
* @return effect producing TestRandom instance
573
*/
574
def testRandom: URIO[TestRandom, TestRandom]
575
576
/**
577
* Access TestRandom and apply function
578
* @param f function to apply to TestRandom
579
* @return effect with TestRandom applied to function
580
*/
581
def testRandomWith[R, E, A](f: TestRandom => ZIO[R, E, A]): ZIO[R with TestRandom, E, A]
582
583
/**
584
* Access TestSystem service
585
* @return effect producing TestSystem instance
586
*/
587
def testSystem: URIO[TestSystem, TestSystem]
588
589
/**
590
* Access TestSystem and apply function
591
* @param f function to apply to TestSystem
592
* @return effect with TestSystem applied to function
593
*/
594
def testSystemWith[R, E, A](f: TestSystem => ZIO[R, E, A]): ZIO[R with TestSystem, E, A]
595
```
596
597
**Usage Examples:**
598
599
```scala
600
import zio.test._
601
import zio._
602
603
// Direct service access
604
test("service access patterns") {
605
for {
606
clock <- testClock
607
_ <- clock.adjust(1.hour)
608
console <- testConsole
609
_ <- console.feedLines("test input")
610
random <- testRandom
611
_ <- random.setSeed(123L)
612
system <- testSystem
613
_ <- system.putEnv("TEST_VAR", "test_value")
614
} yield assertTrue(true)
615
}
616
617
// Service access with functions
618
test("service access with functions") {
619
for {
620
_ <- testClockWith(_.adjust(2.hours))
621
output <- testConsoleWith { console =>
622
Console.printLine("Hello") *> console.output
623
}
624
randomValue <- testRandomWith { random =>
625
random.setSeed(456L) *> Random.nextInt(100)
626
}
627
envVar <- testSystemWith { system =>
628
system.putEnv("VAR", "value") *> System.env("VAR")
629
}
630
} yield assertTrue(
631
output.contains("Hello") &&
632
randomValue >= 0 && randomValue < 100 &&
633
envVar.contains("value")
634
)
635
}
636
```
637
638
### Live Service Integration
639
640
Working with live services when test services are insufficient.
641
642
```scala { .api }
643
/**
644
* Execute effect with live clock instead of test clock
645
* @param zio effect to execute with live clock
646
* @return effect executed with live clock service
647
*/
648
def live[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]
649
650
/**
651
* Execute effect with live services replacing test services
652
* @param zio effect to execute with live services
653
* @return effect executed with live service implementations
654
*/
655
def withLive[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]
656
```
657
658
**Usage Examples:**
659
660
```scala
661
import zio.test._
662
import zio._
663
664
// Mix test and live services
665
test("performance measurement with real time") {
666
for {
667
// Use test clock for most of the test
668
testClock <- ZIO.service[TestClock]
669
_ <- testClock.setTime(Duration.fromMillis(1000000L))
670
671
// Use live clock for actual timing measurement
672
start <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
673
_ <- live(ZIO.sleep(100.millis)) // Actually sleep for 100ms
674
end <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
675
676
duration = end - start
677
} yield assertTrue(duration >= 100L && duration < 200L)
678
}
679
680
// Test with real console for debugging
681
test("debug with real console") {
682
for {
683
_ <- Console.printLine("This goes to test console")
684
_ <- live(Console.printLine("This goes to real console for debugging"))
685
testConsole <- ZIO.service[TestConsole]
686
output <- testConsole.output
687
} yield assertTrue(output.size == 1) // Only test console output is captured
688
}
689
```