ZIO is a type-safe, composable library for asynchronous and concurrent programming in Scala (from: The ZIO github). The ZIO framework provides your program as immutable and pure values, which are very simple to properly unit test. But how can you run an integration test to see if your application starts up properly?

Our application

We have a simple application, which starts a simple rest service using libraries: ZIO, http4s, and circe for json parsing. Our Main app looks like this:

Listing 1. the main application
import com.jdriven.api.Api
import zio.{App, RIO, ZIO}

object Main extends App {
  def run(args: List[String]): ZIO[Clock, Nothing, Int] = myAppLogic.fold(_ => 1, _ => 0)

  // The Clock is needed by the http4s server
  private val myAppLogic: RIO[Clock, Unit] = ZIO.runtime[Clock]
    .flatMap(implicit rtc => Api.createServer)
}

The API code is not relevant for this post (find the complete code here), except that it provides a GET endpoint /health which simply returns the following if the service is running:

Listing 2. a healthy response
{
    "health": "UP"
}

In our test we just want to call this endpoint against a running Main application, and assert that we get the desired response.

Setting up the dependencies

To use the ZIO Test test framework, we need to add two dependencies, and add ZioTest to the testFrameworks in sbt:

Listing 3. build.sbt
libraryDependencies ++= Seq(
  "dev.zio" %% "zio-test"     % zioVersion % "test",
  "dev.zio" %% "zio-test-sbt" % zioVersion % "test"
),

// Note: if you use other test frameworks as well, use '++=' instead of ':='
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))

Writing our test

A simple test with the ZIO Test framework could look like this:

Listing 4. a simple test
object MainTest extends DefaultRunnableSpec(
      suite("A started Main")(
        testM("should be healthy") {
          // your test code here
        }
      )
    )

A test should extend DefaultRunnableSpec and provide your suite containing the tests directly in its constructor. To execute http calls, we need an HTTP client, which we can create using http4s. We wrap the created client in a zio.Task to use it in the next step in our for-comprehension to execute the HTTP request. At last we create the expected json result to yield an Assertion (note that is not needed as we cannot flatMap over its result).

Now, since we’re writing an integration test, we need to actually start the application in our test. We can use the zio.Runtime from our Main to call Main.unsafeRun(Main.run(List()).fork) in the body of our test object. We need to fork, so our HTTP server application is not blocking the main thread. We now have one running Main application before the test suite is started.

Listing 5. the final integration test
object MainTest extends DefaultRunnableSpec(
      suite("A started Main")(
        testM("should be healthy") {
          for {
            client   <- Task(JavaNetClientBuilder[Task](blocker).create)
            response <- client.expect[Json]("http://localhost:8080/health")
            expected = Json.obj(("health", Json.fromString("UP")))
          } yield assert(response, equalTo(expected))
        }
      )
    ) {
  Main.unsafeRun(Main.run(List()).fork)
}

// we need this because http4s is built upon cats.effect
object TestHelpers {
  val blockingEC = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5))
  val blocker    = Blocker.liftExecutionContext(blockingEC)
}

Happy testing.

The complete code example can be found on GitLab.

shadow-left