A simple integration test using Scala and ZIO
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:
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:
{
"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:
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:
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.
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.