Skip to main content
Version: ZIO 2.x

Why ZIO Test?

In this section, we will discuss important features of the ZIO Test which help us to test our effectual code easily.

Test Environment

The library includes built-in testable versions of all the standard ZIO services (Clock, Console, System, and Random). For example, the TestClock has some timing actions that enables us to control the passage of time. So instead of waiting for timeouts and passage of time, we can adjust the time in our test:

import zio._
import zio.test.{test, _}
import zio.test.Assertion._

test("timeout") {
for {
fiber <- ZIO.sleep(5.minutes).timeout(1.minute).fork
_ <- TestClock.adjust(1.minute)
result <- fiber.join
} yield assertTrue(result.isEmpty)

In this example, to test the timeout function without waiting for one minute, we passed the time for one minute using the adjust operation. Sometimes, we may want to run these kinds of tests with the nonFlaky operator, which runs the test one hundred different times.

The TestRandom service has some extra functionality that enables us to test those functionalities with randomness. We can provide seed number to the TestRandom, and then we can have an exact expectation of the random function results.

Each of these services, comes with a bunch of functionality that makes it very easy to test effects.

Whenever we need to access the live environment, we can use the live method in the test package or test annotations like withLiveConsole.

Resource Management

We may need to set up and tear down some fixtures in our test code before and after running tests. ZIO Test manages this seamlessly for us. So, instead of providing before/after, beforeAll/afterAll hooks which are not composable, we can provide a ZLayer to each test or a test suite. The ZIO test takes care of acquiring, utilizing, and releasing that layer.

For example, if we have a Kafka layer, we can provide it to one test, or we can provide it to an entire suite of tests, just like the example below:

suite("a test suite with shared kafka layer")(test1, test2, test3)

This layer going to get acquired once, then we have access to that service within all these three tests within the suite and then it is guaranteed to be released at the end of our tests.

So in ZIO Test, we have nice resource management which enables us to have tests where:

  • They are resource safe
  • Resources can be acquired and released per test or across a suite of tests
  • Fully composable

Property Based Testing

Support for property based testing is included out-of-the-box through the check method and its variants and the Gen and Sample classes. For example, here is how we could write a property to test that integer addition is associative.

import zio.test._

val associativity =
check(,, { (x, y, z) =>
assertTrue(((x + y) + z) == (x + (y + z)))

If a property fails, the failure will be automatically shrunk to the smallest failing cases to make it easier for us to diagnose the problem. And shrinking is integrated with the generation of random variables, so we are guaranteed that any shrunk counter example will meet the conditions of our original generator.

ZIO Test also supports automatic derivation of generators using the ZIO Test Magnolia module:

import zio._
import zio.test._
import zio.test.magnolia._

case class Point(x: Double, y: Double)

val genPoint: Gen[Any, Point] = DeriveGen[Point]

sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color

val genColor: Gen[Any, Color] = DeriveGen[Color]

Test Reporting

When tests do fail, it is easy to see what went wrong because the test reporter will show us the entire assertion that failed and the specific part of the assertion that failed. To facilitate this, a variety of assertion combinators are included in the Assertion class.

Test Aspects

Test aspects are powerful tools for modifying behavior of individual tests or even entire suites that we have already written. Convenient syntax @@ is provided for applying test aspects.

For example, we can apply a timeout to a test by using test @@ timeout(60.seconds) or only run a test on JavaScript by using test @@ jsOnly.

Test aspects are highly composable, so we can combine multiple test aspects together:

import zio.test._
import zio.test.TestAspect._

test("another zio test")(???) @@ timeout(60.seconds) @@ jvmOnly

Zero Dependencies

As a library with zero third party dependencies, this project is available on the JVM, ScalaJS, Dotty, and will be available on Scala Native in the near future. So we can write our tests once and make sure that our code works correctly across all platforms that we support.

JUnit integration

A custom JUnit runner is provided for running ZIO Test specs under other build tools (like Maven, Gradle, Bazel, etc.) and under IDEs.

To get the runner, we need to add the equivalent of following dependency definition under our build tool:

"dev.zio" %% "zio-test-junit" % zioVersion % "test"

To make our spec appear as a JUnit test to build tools and IDEs, we should convert it to a class (JUnit won't run scala objects) and annotate it with @RunWith(classOf[zio.test.junit.ZTestJUnitRunner]) or simply extend zio.test.junit.JUnitRunnableSpec.

See ExampleSpecWithJUnit

SBT (and thus Scala.JS) is not supported, as the JUnit Test Framework for SBT doesn't seem to support custom runners.