or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assertions.mdconfiguration.mdindex.mdproperty-testing.mdtest-aspects.mdtest-definition.mdtest-services.md
tile.json

test-services.mddocs/

Test Services

Deterministic test services that replace system services during testing for predictable, reproducible test execution.

Capabilities

TestClock

Deterministic clock service for controlling time progression in tests.

/**
 * Test clock that allows manual time control
 */
trait TestClock extends Clock {
  /**
   * Advance the clock by the specified duration
   * @param duration amount to advance time
   * @return effect that advances the clock
   */
  def adjust(duration: Duration): UIO[Unit]
  
  /**
   * Set the clock to an absolute time
   * @param duration absolute time from epoch
   * @return effect that sets the clock time
   */
  def setTime(duration: Duration): UIO[Unit]
  
  /**
   * Get the current time zone
   * @return current time zone
   */
  def timeZone: UIO[ZoneId]
  
  /**
   * Set the time zone for the clock
   * @param zone new time zone
   * @return effect that sets the time zone
   */
  def setTimeZone(zone: ZoneId): UIO[Unit]
  
  /**
   * Sleep for the specified duration (uses test time)
   * @param duration duration to sleep
   * @return effect that completes after the duration
   */
  override def sleep(duration: Duration): UIO[Unit]
  
  /**
   * Get current time in specified time unit
   * @param unit time unit for the result
   * @return current time in the specified unit
   */
  override def currentTime(unit: TimeUnit): UIO[Long]
  
  /**
   * Get current time as instant
   * @return current time instant
   */
  override def instant: UIO[Instant]
}

object TestClock {
  /**
   * Default test clock implementation starting at epoch
   */
  val default: ULayer[TestClock]
  
  /**
   * Create test clock starting at specified time
   * @param startTime initial clock time
   * @return layer providing test clock
   */
  def live(startTime: Instant): ULayer[TestClock]
}

Usage Examples:

import zio.test._
import zio._
import java.time.Instant
import java.util.concurrent.TimeUnit

// Basic time manipulation
test("time-dependent operation") {
  for {
    clock <- ZIO.service[TestClock]
    start <- clock.currentTime(TimeUnit.MILLISECONDS)
    _ <- clock.adjust(1.hour)
    end <- clock.currentTime(TimeUnit.MILLISECONDS)
  } yield assertTrue(end - start == 3600000)
}

// Testing scheduled operations
test("scheduled task execution") {
  for {
    clock <- ZIO.service[TestClock]
    ref <- Ref.make(0)
    // Schedule task to run every minute
    fiber <- (ZIO.sleep(1.minute) *> ref.update(_ + 1)).forever.fork
    // Advance time and check execution
    _ <- clock.adjust(5.minutes)
    count <- ref.get
    _ <- fiber.interrupt
  } yield assertTrue(count == 5)
}

// Testing timeout behavior
test("operation timeout") {
  for {
    clock <- ZIO.service[TestClock]
    fiber <- (ZIO.sleep(10.seconds) *> ZIO.succeed("completed")).fork
    _ <- clock.adjust(5.seconds)
    result <- fiber.poll
  } yield assertTrue(result.isEmpty) // Should still be running
}

TestConsole

Console service for testing input/output operations with configurable responses.

/**
 * Test console that captures output and provides controllable input
 */
trait TestConsole extends Console {
  /**
   * Get all captured output lines
   * @return vector of output lines
   */
  def output: UIO[Vector[String]]
  
  /**
   * Clear all captured output
   * @return effect that clears output buffer
   */
  def clearOutput: UIO[Unit]
  
  /**
   * Provide input lines for console reading
   * @param lines input lines to feed to console
   * @return effect that feeds the lines
   */
  def feedLines(lines: String*): UIO[Unit]
  
  /**
   * Get all captured error output
   * @return vector of error output lines
   */
  def errorOutput: UIO[Vector[String]]
  
  /**
   * Clear all captured error output
   * @return effect that clears error output buffer
   */
  def clearErrorOutput: UIO[Unit]
  
  /**
   * Print line to output (captured)
   * @param line line to print
   * @return effect that prints the line
   */
  override def printLine(line: Any): IO[IOException, Unit]
  
  /**
   * Print to output without newline (captured)
   * @param text text to print
   * @return effect that prints the text
   */
  override def print(text: Any): IO[IOException, Unit]
  
  /**
   * Read line from input (uses fed lines)
   * @return effect that reads a line
   */
  override def readLine: IO[IOException, String]
}

object TestConsole {
  /**
   * Debug console that prints to real console and captures
   */
  val debug: ULayer[TestConsole]
  
  /**
   * Silent console that only captures without real output
   */
  val silent: ULayer[TestConsole]
}

Usage Examples:

import zio.test._
import zio._
import java.io.IOException

// Capture and verify output
test("application output") {
  for {
    _ <- Console.printLine("Hello, World!")
    _ <- Console.printLine("ZIO Test is awesome!")
    console <- ZIO.service[TestConsole]
    output <- console.output
  } yield assertTrue(
    output == Vector("Hello, World!", "ZIO Test is awesome!")
  )
}

// Test interactive console programs
test("interactive program") {
  def interactiveProgram: ZIO[Console, IOException, String] = 
    for {
      _ <- Console.printLine("What's your name?")
      name <- Console.readLine
      _ <- Console.printLine(s"Hello, $name!")
    } yield name

  for {
    console <- ZIO.service[TestConsole]
    _ <- console.feedLines("Alice")
    result <- interactiveProgram
    output <- console.output
  } yield assertTrue(
    result == "Alice" &&
    output == Vector("What's your name?", "Hello, Alice!")
  )
}

// Test error output
test("error logging") {
  def logError(message: String): ZIO[Console, IOException, Unit] =
    Console.printLineError(s"ERROR: $message")

  for {
    _ <- logError("Something went wrong")
    console <- ZIO.service[TestConsole]
    errorOutput <- console.errorOutput
  } yield assertTrue(errorOutput == Vector("ERROR: Something went wrong"))
}

TestRandom

Deterministic random service for reproducible random number generation in tests.

/**
 * Test random service with controllable seed and predefined values
 */
trait TestRandom extends Random {
  /**
   * Set the random seed for reproducible generation
   * @param seed random seed value
   * @return effect that sets the seed
   */
  def setSeed(seed: Long): UIO[Unit]
  
  /**
   * Feed predefined boolean values
   * @param booleans boolean values to return from nextBoolean
   * @return effect that feeds the values
   */
  def feedBooleans(booleans: Boolean*): UIO[Unit]
  
  /**
   * Clear all fed boolean values
   * @return effect that clears boolean buffer
   */
  def clearBooleans: UIO[Unit]
  
  /**
   * Feed predefined integer values
   * @param ints integer values to return from nextInt
   * @return effect that feeds the values
   */
  def feedInts(ints: Int*): UIO[Unit]
  
  /**
   * Clear all fed integer values
   * @return effect that clears integer buffer
   */
  def clearInts: UIO[Unit]
  
  /**
   * Feed predefined long values
   * @param longs long values to return from nextLong
   * @return effect that feeds the values
   */
  def feedLongs(longs: Long*): UIO[Unit]
  
  /**
   * Clear all fed long values
   * @return effect that clears long buffer
   */
  def clearLongs: UIO[Unit]
  
  /**
   * Feed predefined double values
   * @param doubles double values to return from nextDouble
   * @return effect that feeds the values
   */
  def feedDoubles(doubles: Double*): UIO[Unit]
  
  /**
   * Clear all fed double values
   * @return effect that clears double buffer
   */
  def clearDoubles: UIO[Unit]
  
  /**
   * Generate next boolean (uses fed values or seed-based generation)
   * @return effect producing boolean value
   */
  override def nextBoolean: UIO[Boolean]
  
  /**
   * Generate next integer in range (uses fed values or seed-based generation)
   * @param n upper bound (exclusive)
   * @return effect producing integer value
   */
  override def nextInt(n: Int): UIO[Int]
  
  /**
   * Generate next long (uses fed values or seed-based generation)
   * @return effect producing long value
   */
  override def nextLong: UIO[Long]
  
  /**
   * Generate next double (uses fed values or seed-based generation)
   * @return effect producing double value
   */
  override def nextDouble: UIO[Double]
}

object TestRandom {
  /**
   * Deterministic random with fixed seed
   * @param seed initial seed value
   * @return layer providing test random
   */
  def deterministic(seed: Long): ULayer[TestRandom]
  
  /**
   * Deterministic random with default seed (0)
   */
  val deterministic: ULayer[TestRandom]
}

Usage Examples:

import zio.test._
import zio._

// Reproducible random testing
test("random behavior with fixed seed") {
  def randomOperation: ZIO[Random, Nothing, List[Int]] =
    ZIO.collectAll(List.fill(5)(Random.nextInt(100)))

  for {
    random <- ZIO.service[TestRandom]
    _ <- random.setSeed(12345L)
    result1 <- randomOperation
    _ <- random.setSeed(12345L)
    result2 <- randomOperation
  } yield assertTrue(result1 == result2) // Same seed = same results
}

// Control random values explicitly
test("specific random sequence") {
  def coinFlips(n: Int): ZIO[Random, Nothing, List[String]] =
    ZIO.collectAll(
      List.fill(n)(Random.nextBoolean.map(if (_) "heads" else "tails"))
    )

  for {
    random <- ZIO.service[TestRandom]
    _ <- random.feedBooleans(true, false, true, true, false)
    flips <- coinFlips(5)
  } yield assertTrue(
    flips == List("heads", "tails", "heads", "heads", "tails")
  )
}

// Test random distributions
test("random distribution properties") {
  def generateSample: ZIO[Random, Nothing, List[Int]] =
    ZIO.collectAll(List.fill(1000)(Random.nextInt(100)))

  for {
    random <- ZIO.service[TestRandom]
    _ <- random.setSeed(42L)
    sample <- generateSample
    average = sample.sum.toDouble / sample.size
  } yield assertTrue(average >= 40.0 && average <= 60.0) // Should be around 50
}

TestSystem

System service for testing environment variables and system properties.

/**
 * Test system service with controllable environment and properties
 */
trait TestSystem extends System {
  /**
   * Set environment variable for testing
   * @param name variable name
   * @param value variable value
   * @return effect that sets the variable
   */
  def putEnv(name: String, value: String): UIO[Unit]
  
  /**
   * Clear environment variable
   * @param name variable name to clear
   * @return effect that clears the variable
   */
  def clearEnv(name: String): UIO[Unit]
  
  /**
   * Set system property for testing
   * @param name property name
   * @param value property value
   * @return effect that sets the property
   */
  def putProperty(name: String, value: String): UIO[Unit]
  
  /**
   * Clear system property
   * @param name property name to clear
   * @return effect that clears the property
   */
  def clearProperty(name: String): UIO[Unit]
  
  /**
   * Get environment variable
   * @param name variable name
   * @return effect producing optional variable value
   */
  override def env(name: String): IO[SecurityException, Option[String]]
  
  /**
   * Get system property
   * @param name property name
   * @return effect producing optional property value
   */
  override def property(name: String): IO[Throwable, Option[String]]
  
  /**
   * Get line separator for current system
   * @return line separator string
   */
  override def lineSeparator: UIO[String]
}

object TestSystem {
  /**
   * Default test system implementation
   */
  val default: ULayer[TestSystem]
  
  /**
   * Test system with initial environment and properties
   * @param env initial environment variables
   * @param props initial system properties
   * @return layer providing test system
   */
  def live(
    env: Map[String, String] = Map.empty,
    props: Map[String, String] = Map.empty
  ): ULayer[TestSystem]
}

Usage Examples:

import zio.test._
import zio._

// Test environment-dependent behavior
test("application configuration from environment") {
  def getConfig: ZIO[System, SecurityException, AppConfig] =
    for {
      host <- System.env("DB_HOST").map(_.getOrElse("localhost"))
      port <- System.env("DB_PORT").map(_.getOrElse("5432"))
    } yield AppConfig(host, port.toInt)

  for {
    system <- ZIO.service[TestSystem]
    _ <- system.putEnv("DB_HOST", "test-db.example.com")
    _ <- system.putEnv("DB_PORT", "3306")
    config <- getConfig
  } yield assertTrue(
    config.host == "test-db.example.com" &&
    config.port == 3306
  )
}

// Test system property behavior
test("debug mode from system property") {
  def isDebugMode: ZIO[System, Throwable, Boolean] =
    System.property("debug").map(_.contains("true"))

  for {
    system <- ZIO.service[TestSystem]
    _ <- system.putProperty("debug", "true")
    debug1 <- isDebugMode
    _ <- system.clearProperty("debug")
    debug2 <- isDebugMode
  } yield assertTrue(debug1 && !debug2)
}

// Test cross-platform line separator handling
test("text file generation") {
  def generateTextFile(lines: List[String]): ZIO[System, Nothing, String] =
    for {
      separator <- System.lineSeparator
    } yield lines.mkString(separator)

  for {
    result <- generateTextFile(List("line1", "line2", "line3"))
  } yield assertTrue(result.contains("line1") && result.contains("line2"))
}

case class AppConfig(host: String, port: Int)

Service Access Functions

Convenience functions for accessing test services from the test environment.

/**
 * Access TestClock service
 * @return effect producing TestClock instance
 */
def testClock: URIO[TestClock, TestClock]

/**
 * Access TestClock and apply function
 * @param f function to apply to TestClock
 * @return effect with TestClock applied to function
 */
def testClockWith[R, E, A](f: TestClock => ZIO[R, E, A]): ZIO[R with TestClock, E, A]

/**
 * Access TestConsole service
 * @return effect producing TestConsole instance
 */
def testConsole: URIO[TestConsole, TestConsole]

/**
 * Access TestConsole and apply function
 * @param f function to apply to TestConsole
 * @return effect with TestConsole applied to function
 */
def testConsoleWith[R, E, A](f: TestConsole => ZIO[R, E, A]): ZIO[R with TestConsole, E, A]

/**
 * Access TestRandom service
 * @return effect producing TestRandom instance
 */
def testRandom: URIO[TestRandom, TestRandom]

/**
 * Access TestRandom and apply function
 * @param f function to apply to TestRandom
 * @return effect with TestRandom applied to function
 */
def testRandomWith[R, E, A](f: TestRandom => ZIO[R, E, A]): ZIO[R with TestRandom, E, A]

/**
 * Access TestSystem service
 * @return effect producing TestSystem instance
 */
def testSystem: URIO[TestSystem, TestSystem]

/**
 * Access TestSystem and apply function
 * @param f function to apply to TestSystem
 * @return effect with TestSystem applied to function
 */
def testSystemWith[R, E, A](f: TestSystem => ZIO[R, E, A]): ZIO[R with TestSystem, E, A]

Usage Examples:

import zio.test._
import zio._

// Direct service access
test("service access patterns") {
  for {
    clock <- testClock
    _ <- clock.adjust(1.hour)
    console <- testConsole
    _ <- console.feedLines("test input")
    random <- testRandom
    _ <- random.setSeed(123L)
    system <- testSystem
    _ <- system.putEnv("TEST_VAR", "test_value")
  } yield assertTrue(true)
}

// Service access with functions
test("service access with functions") {
  for {
    _ <- testClockWith(_.adjust(2.hours))
    output <- testConsoleWith { console =>
      Console.printLine("Hello") *> console.output
    }
    randomValue <- testRandomWith { random =>
      random.setSeed(456L) *> Random.nextInt(100)
    }
    envVar <- testSystemWith { system =>
      system.putEnv("VAR", "value") *> System.env("VAR")
    }
  } yield assertTrue(
    output.contains("Hello") &&
    randomValue >= 0 && randomValue < 100 &&
    envVar.contains("value")
  )
}

Live Service Integration

Working with live services when test services are insufficient.

/**
 * Execute effect with live clock instead of test clock
 * @param zio effect to execute with live clock
 * @return effect executed with live clock service
 */
def live[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]

/**
 * Execute effect with live services replacing test services
 * @param zio effect to execute with live services
 * @return effect executed with live service implementations
 */
def withLive[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A]

Usage Examples:

import zio.test._
import zio._

// Mix test and live services
test("performance measurement with real time") {
  for {
    // Use test clock for most of the test
    testClock <- ZIO.service[TestClock]
    _ <- testClock.setTime(Duration.fromMillis(1000000L))
    
    // Use live clock for actual timing measurement
    start <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
    _ <- live(ZIO.sleep(100.millis)) // Actually sleep for 100ms
    end <- live(Clock.currentTime(TimeUnit.MILLISECONDS))
    
    duration = end - start
  } yield assertTrue(duration >= 100L && duration < 200L)
}

// Test with real console for debugging
test("debug with real console") {
  for {
    _ <- Console.printLine("This goes to test console")
    _ <- live(Console.printLine("This goes to real console for debugging"))
    testConsole <- ZIO.service[TestConsole]
    output <- testConsole.output
  } yield assertTrue(output.size == 1) // Only test console output is captured
}