Testing HTTP Applications
Testing HTTP applications is a critical part of the development process. Utilizing the ZIO Test we can write first-class tests for our HTTP applications.
ZIO Test
We have comprehensive documentation on ZIO Test which is worth reading to understand how to write tests using ZIO effects.
It is easy to test ZIO HTTP applications because we can think of Routes
as a function of Request => ZIO[R, Response, Response]
. By provide a Request
to Routes#runZIO
will output a Response
. Without starting a server:
import zio.test._
import zio.test.Assertion.equalTo
import zio.http._
object ExampleSpec extends ZIOSpecDefault {
def spec = suite("http")(
test("should be ok") {
val app = Handler.ok.toRoutes
val req = Request.get(URL(Path.root))
assertZIO(app.runZIO(req))(equalTo(Response.ok))
}
)
}
ZIO HTTP Testkit
ZIO HTTP provides the zio-http-testkit
package that includes a TestClient
and a TestServer
.
libraryDependencies += "dev.zio" %% "zio-test" % "2.1.9" % Test
libraryDependencies += "dev.zio" %% "zio-test-sbt" % "2.1.9" % Test
libraryDependencies += "dev.zio" %% "zio-http-testkit" % "3.0.1" % Test
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
Now, based on the requirement we can use any of the following test utilities:
TestClient
The TestClient
allows us to write tests for our HTTP applications by defining the behavior of the client:
TestClient.addRequestResponse
- Adds an 1-1 mapping from aRequest
to aResponse
to theTestClient
.TestClient.addRoute
andaddRoutes
- Add one or moreRoute
or a singleRoutes
instance to theTestClient
.TestClient.installSocketApp
- Installs aWebSocketApp
to theTestClient
.
After defining the behavior of the test client, we can use the TestClient.layer
to provide the TestClient
and Client
to the test cases:
import zio._
import zio.http._
import zio.test.{test, _}
object TestUsingTestClient extends ZIOSpecDefault {
def spec =
test("hello world route and fallback") {
for {
client <- ZIO.service[Client]
_ <- TestClient.addRoutes {
Routes(
Method.GET / trailing -> handler { Response.text("fallback") },
Method.GET / "hello" / "world" -> handler { Response.text("Hey there!") },
)
}
helloResponse <- client.batched(Request.get(URL.root / "hello" / "world"))
helloBody <- helloResponse.body.asString
fallbackResponse <- client.batched(Request.get(URL.root / "any"))
fallbackBody <- fallbackResponse.body.asString
} yield assertTrue(helloBody == "Hey there!", fallbackBody == "fallback")
}.provide(TestClient.layer)
}
TestServer
The TestServer
allows us to write tests for our HTTP applications by defining the behavior of the server:
TestServer.addRequestResponse
- Adds an 1-1 mapping from aRequest
to aResponse
to theTestServer
.TestServer.addRoute
andTestServer.addRoutes
- Add one or moreRoute
or a singleRoutes
instance to theTestServer
.
After defining the behavior of the test server, we can use the TestServer.layer
to provide the TestServer
to any test cases that require Server
:
import zio._
import zio.http._
import zio.http.netty.NettyConfig
import zio.http.netty.server.NettyDriver
import zio.test._
object TestServerExampleSpec extends ZIOSpecDefault {
def spec = suite("test http app") {
test("test hello and fallback routes") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)
testRequest = Request
.get(url = URL.root.port(port))
.addHeaders(Headers(Header.Accept(MediaType.text.`plain`)))
_ <- TestServer.addRoutes {
Routes(
Method.GET / trailing -> handler {
Response.text("fallback")
},
Method.GET / "hello" / "world" -> handler {
Response.text("Hey there!")
},
)
}
helloResponse <- client.batched(Request.get(testRequest.url / "hello" / "world"))
helloBody <- helloResponse.body.asString
fallbackResponse <- client.batched(Request.get(testRequest.url / "any"))
fallbackBody <- fallbackResponse.body.asString
} yield assertTrue(helloBody == "Hey there!", fallbackBody == "fallback")
}.provideSome[Client with Driver](TestServer.layer)
}.provide(
ZLayer.succeed(Server.Config.default.onAnyOpenPort),
Client.default,
NettyDriver.customized,
ZLayer.succeed(NettyConfig.defaultWithFastShutdown),
)
}