How to write unit test in Scala ZIO

Businessman playing digital tablet in cafe
Reading Time: 3 minutes

ZIO is a Scala library for creating asynchronous, concurrent, and fault-tolerant applications. It is based on the concept of “effects” and provides a powerful and flexible way to express and compose computations that may have side effects, such as reading from a file, writing to a database, or making an HTTP request.

ZIO offers a full-featured ecosystem for building applications, including libraries for common tasks such as configuration, logging, testing, and monitoring. It also provides a type-safe and functional approach to error handling, which makes it easy to write reliable and resilient applications.

How to write UT in ZIO:

To write unit tests in Scala using ZIO, you can use the zio-test library, which provides a powerful and flexible testing framework for ZIO programs. The zio-test library allows you to write unit tests that can run ZIO effects, verify their results, and simulate different scenarios such as failures, timeouts, and interruptions.

To use the zio-test library in your Scala project, you can add the following dependency to your build.sbt file:

libraryDependencies += "dev.zio" %% "zio-test" % "2.0.5"

This dependency adds the zio-test library to your project and configures it to be used only for testing. You can then import the zio.test._ and zio.test.environment._ packages. Also you can use the testM and environment methods to define and run your unit tests.

For example, suppose you have a ZIO program that reads a line of input from the console and returns it as a result:

import zio._
import zio.console._

object Program {

  val readLine: ZIO[Console, IOException, String] =

    ZIO.effect(scala.io.StdIn.readLine())

  val program: ZIO[Console, IOException, String] =

    for {

      line <- readLine

    } yield line

}

You can write a unit test for this program using the testM and environment methods as follows:

import zio._
import zio.console._
import zio.test._
import zio.test.environment._

object ProgramTest extends DefaultRunnableSpec {

  def spec: ZSpec[TestEnvironment, Any] =

    suite("Program")(

      testM("should read a line of input from the console") {

        for {

          _     <- TestConsole.feedLines("hello, world!")

          value <- Program.program

        } yield assert(value)(equalTo("hello, world!"))

      }

    )

}

This code defines a ProgramTest object that extends the DefaultRunnableSpec trait. It defines a spec method that contains the unit tests for the Program object. The suite and testM methods are used to define a test suite and a test case, respectively. The testM method takes a description of the test case and a ZIO[TestEnvironment, TestFailure[E], Unit] value that contains the test logic.

In this code, the TestConsole.feedLines method is used to simulate the input provided by the user to the Program.program effect. The feedLines method returns a ZIO[TestConsole, TestFailure[Nothing], Unit] value. It feeds the specified lines of input to the TestConsole environment, which is passed to the Program.program effect when it is run.

The Program.program effect is then run and its result is stored in the value variable. The assert method is used to verify that the value of the value variable is equal to the expected result. The equalTo method is used to compare the actual and expected result/value.

Another example is regarding check method. You can use the check method which is provided by the zio.test package. This method takes a test description as a string and a ZIO value representing the test case. It will run the test and report the result. Here’s an example

import zio._
import zio.test._
import zio.test.Assertion._

val square: Int => Int = x => x * x

def squareSpec: ZIO[Any, Nothing, TestResult] =

  check(test("square test") {

    assert(square(2), equalTo(4))

  })

In this example, the squareSpec function defines a unit test for the square function. It takes an integer and returns its square. The test uses the assert method provided by zio.test to check that the result of calling square with the input 2 is equal to 4.

To run the test, you can use the run method provided by zio.test:

val result: TestResult = squareSpec.run

This will run the test and return a TestResult value, which indicates whether the test passed or failed. You can then use the assertions and failures methods provided by TestResult to get the number of assertions made and the number of failures, respectively.

Conclusion:

This blog is only a simple approach for learning purposes. It will be used in plenty of useful places. As we are learning ZIO, there is a lot more in this vast library. In future blogs on the same topic, we’ll see how to better use this testing framework for writing unit tests on complicated methods with side-effects.

If you want to add anything or you do not relate to my view on any point, drop me a comment. I will be happy to discuss it. For more blogs, click here

Written by 

Rituraj Khare is a Software Consultant at Knoldus Software LLP. An avid Scala programmer and Big Data engineer, he has experience with the tech stack such as - Scala| Spark| Kafka| Python| Unit testing| Git| Jenkins| Grafana.