Testing
Introduction
This document implements a simple org.http4s.HttpService
and then
walk through the results of applying inputs, i.e. org.http4s.Request
, to the Service, i.e. org.http4s.HttpService
.
After reading this doc, the reader should feel comfortable writing a unit test using his/her favorite Scala testing library.
Now, let’s define an org.http4s.HttpService
.
import cats.implicits._
import io.circe._
import io.circe.syntax._
import io.circe.generic.semiauto._
import cats.effect._, org.http4s._, org.http4s.dsl.io._
import org.http4s.circe._
import org.http4s.dsl.io._
case class User(name: String, age: Int)
implicit val UserEncoder: Encoder[User] = deriveEncoder[User]
// UserEncoder: Encoder[User] = io.circe.generic.encoding.DerivedObjectEncoder$$anon$1@c88e124
trait UserRepo[F[_]] {
def find(userId: String): F[Option[User]]
}
def service[F[_]](repo: UserRepo[F])(
implicit F: Effect[F]
): HttpService[F] = HttpService[F] {
case GET -> Root / "user" / id =>
repo.find(id).flatMap {
case Some(user) => Response(status = Status.Ok).withBody(user.asJson)
case None => F.pure(Response(status = Status.NotFound))
}
}
For testing, let’s define a check
function:
// Return true if match succeeds; otherwise false
def check[A](actual: IO[Response[IO]],
expectedStatus: Status,
expectedBody: Option[A])(
implicit ev: EntityDecoder[IO, A]
): Boolean = {
val actualResp = actual.unsafeRunSync
val statusCheck = actualResp.status == expectedStatus
val bodyCheck = expectedBody.fold[Boolean](
actualResp.body.compile.toVector.unsafeRunSync.isEmpty)( // Verify Response's body is empty.
expected => actualResp.as[A].unsafeRunSync == expected
)
statusCheck && bodyCheck
}
Let’s define service by passing a UserRepo
that returns Ok(user)
.
val success: UserRepo[IO] = new UserRepo[IO] {
def find(id: String): IO[Option[User]] = IO.pure(Some(User("johndoe", 42)))
}
// success: UserRepo[IO] = repl.Session$App$$anon$3@60cb78fe
val response1: IO[Response[IO]] = service[IO](success).orNotFound.run(
Request(method = Method.GET, uri = Uri.uri("/user/not-used") )
)
// response1: IO[Response[IO]] = Map(
// Bind(Pure(Some(User("johndoe", 42))), <function1>),
// scala.Function1$$Lambda$12648/922425644@3d5567f0,
// 1
// )
val expectedJson = Json.obj(
("name", Json.fromString("johndoe")),
("age", Json.fromBigInt(42))
)
// expectedJson: Json = JObject(object[name -> "johndoe",age -> 42])
check[Json](response1, Status.Ok, Some(expectedJson))
// res0: Boolean = true
Next, let’s define a service with a userRepo
that returns None
to any input.
val foundNone: UserRepo[IO] = new UserRepo[IO] {
def find(id: String): IO[Option[User]] = IO.pure(None)
}
// foundNone: UserRepo[IO] = repl.Session$App$$anon$4@46e36b4a
val response2: IO[Response[IO]] = service[IO](foundNone).orNotFound.run(
Request(method = Method.GET, uri = Uri.uri("/user/not-used") )
)
// response2: IO[Response[IO]] = Map(
// Bind(Pure(None), <function1>),
// scala.Function1$$Lambda$12648/922425644@d293373,
// 1
// )
check[Json](response2, Status.NotFound, None)
// res1: Boolean = true
Finally, let’s pass a Request
which our service does not handle.
val doesNotMatter: UserRepo[IO] = new UserRepo[IO] {
def find(id: String): IO[Option[User]] = IO.raiseError(new RuntimeException("Should not get called!"))
}
// doesNotMatter: UserRepo[IO] = repl.Session$App$$anon$5@361ea6a0
val response3: IO[Response[IO]] = service[IO](doesNotMatter).orNotFound.run(
Request(method = Method.GET, uri = Uri.uri("/not-a-matching-path") )
)
// response3: IO[Response[IO]] = Map(
// Pure(None),
// cats.data.OptionT$$Lambda$13255/1239170746@20623d74,
// 0
// )
check[String](response3, Status.NotFound, Some("Not found"))
// res2: Boolean = true
Conclusion
The above documentation demonstrated how to define an HttpService[F], pass Request
’s, and then
test the expected Response
.
To add unit tests in your chosen Scala Testing Framework, please follow the above examples.
References