Testing

Introduction

This document implements a simple org.http4s.HttpRoutes 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 cats.implicits._

import io.circe._
// import io.circe._

import io.circe.syntax._
// import io.circe.syntax._

import io.circe.generic.semiauto._
// import io.circe.generic.semiauto._

import cats.effect._
// import cats.effect._

import org.http4s._
// import org.http4s._

import org.http4s.circe._
// import org.http4s.circe._

import org.http4s.dsl.io._
// import org.http4s.dsl.io._

import org.http4s.implicits._
// import org.http4s.implicits._

final case class User(name: String, age: Int) 
// defined class User

implicit val UserEncoder: Encoder[User] = deriveEncoder[User]
// UserEncoder: io.circe.Encoder[User] = io.circe.generic.encoding.DerivedObjectEncoder$$anon$1@2dd07574

trait UserRepo[F[_]] {
  def find(userId: String): F[Option[User]]
}
// defined trait UserRepo

def service[F[_]](repo: UserRepo[F])(
      implicit F: Effect[F]
): HttpRoutes[F] = HttpRoutes.of[F] {
  case GET -> Root / "user" / id =>
    repo.find(id).map {
      case Some(user) => Response(status = Status.Ok).withEntity(user.asJson)
      case None       => Response(status = Status.NotFound)
    }
}
// service: [F[_]](repo: UserRepo[F])(implicit F: cats.effect.Effect[F])org.http4s.HttpRoutes[F]

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   
}
// check: [A](actual: cats.effect.IO[org.http4s.Response[cats.effect.IO]], expectedStatus: org.http4s.Status, expectedBody: Option[A])(implicit ev: org.http4s.EntityDecoder[cats.effect.IO,A])Boolean

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[cats.effect.IO] = $anon$1@5dc5ac69

val response: IO[Response[IO]] = service[IO](success).orNotFound.run(
  Request(method = Method.GET, uri = Uri.uri("/user/not-used") )
)
// response: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = <function1>

val expectedJson = Json.obj(
  ("name", Json.fromString("johndoe")),
  ("age",  Json.fromBigInt(42))
)
// expectedJson: io.circe.Json =
// {
//   "name" : "johndoe",
//   "age" : 42
// }

check[Json](response, Status.Ok, Some(expectedJson))
// res1: 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[cats.effect.IO] = $anon$1@d7e9e2f

val response: IO[Response[IO]] = service[IO](foundNone).orNotFound.run(
  Request(method = Method.GET, uri = Uri.uri("/user/not-used") )
)
// response: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = <function1>

check[Json](response, Status.NotFound, None)
// res2: 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[cats.effect.IO] = $anon$1@4cf91b6a

val response: IO[Response[IO]] = service[IO](doesNotMatter).orNotFound.run(
  Request(method = Method.GET, uri = Uri.uri("/not-a-matching-path") )
)
// response: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = <function1>

check[String](response, Status.NotFound, Some("Not found"))
// res3: 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