Skip to main content

Testing

Setup​

Add the http4s-stir-testkit dependency to your project:

libraryDependencies += "pl.iterators" %% "http4s-stir-testkit" % http4sStirVersion % Test

ScalaTest Integration​

Extend ScalatestRouteTest in your test class. An implicit IORuntime must be provided:

import cats.effect.unsafe.IORuntime
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import pl.iterators.stir.server.Directives._
import pl.iterators.stir.testkit.ScalatestRouteTest

class MyRouteSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {
override implicit val runtime: IORuntime = IORuntime.global

// tests go here
}

Specs2 Integration​

Extend Specs2RouteTest in your specification:

import cats.effect.unsafe.IORuntime
import pl.iterators.stir.server.Directives._
import pl.iterators.stir.testkit.Specs2RouteTest

class MyRouteSpec extends org.specs2.mutable.Specification with Specs2RouteTest {
override implicit val runtime: IORuntime = IORuntime.global

// tests go here
}

The ~> Operator​

The core testing pattern is Request ~> route ~> check { assertions }:

Get("/hello") ~> route ~> check {
status shouldEqual Status.Ok
responseAs[String] shouldEqual "world"
}

The ~> operator applies the request to the route without starting an HTTP server. The route is sealed for exceptions but not for rejections, allowing rejection inspection in tests.

The ~!> Operator​

The ~!> operator runs the route through a fully fledged HTTP server (Ember). This is useful for testing directives that depend on actual server behavior, at the cost of additional overhead:

Get("/hello") ~!> route ~> check {
status shouldEqual Status.Ok
}

The check Block​

Inside a check { } block, the following members are available for assertions:

Response Inspection​

MemberTypeDescription
statusStatusThe HTTP response status code.
responseResponse[IO]The full response object.
responseAs[T]TUnmarshal the response body to type T. Requires an implicit EntityDecoder[IO, T].
entityAs[T]TAlias for responseAs[T].
contentType`Content-Type`The response Content-Type header.
mediaTypeMediaTypeThe media type from the Content-Type.
charsetCharsetThe charset from the Content-Type.
headersHeadersAll response headers.
header[T]Option[F[T]]Retrieve a typed header by type parameter.
header(name)Option[Header.Raw]Retrieve a header by name (case-insensitive).

Rejection Inspection​

MemberTypeDescription
handledBooleantrue if the request was handled (completed), false if rejected.
rejectionsSeq[Rejection]The list of rejections. Fails if the request was completed.
rejectionRejectionThe single rejection. Fails if zero or more than one rejection was produced.

Request Builders​

The following request builders are available. Each accepts a URI string or Uri value, and optionally a request body:

Get("/path")
Post("/path", entity)
Put("/path", entity)
Patch("/path", entity)
Delete("/path")
Options("/path")
Head("/path")

Builders that accept an entity require an implicit EntityEncoder[IO, T] for the body type.

Request Transformers​

Transform requests before sending them to a route:

TransformerDescription
addHeader(header)Add a single header.
addHeader(name, value)Add a header by name and value strings.
addHeaders(first, more*)Add multiple headers.
removeHeader(name)Remove headers by name.
removeHeader[T]Remove headers by typed header type.
removeHeaders(names*)Remove headers by multiple names.
mapHeaders(f)Transform the headers collection with a function.
addCredentials(credentials)Add an Authorization header with the given credentials.

Apply transformers using ~>:

Post("/path", entity) ~> addHeader("X-Custom", "value") ~> route ~> check {
status shouldEqual Status.Ok
}

Complete Example​

import cats.effect.unsafe.IORuntime
import org.http4s.Status
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import pl.iterators.stir.server._
import pl.iterators.stir.server.Directives._
import pl.iterators.stir.testkit.ScalatestRouteTest

class OrderRouteSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {
override implicit val runtime: IORuntime = IORuntime.global

val route =
path("order" / IntNumber) { orderId =>
get {
complete(s"Order $orderId")
} ~
post {
complete(Status.Created, s"Created order $orderId")
}
}

"The order route" should {
"return an order for GET requests" in {
Get("/order/42") ~> route ~> check {
status shouldEqual Status.Ok
responseAs[String] shouldEqual "Order 42"
}
}
"create an order for POST requests" in {
Post("/order/42") ~> route ~> check {
status shouldEqual Status.Created
}
}
"reject PUT requests" in {
Put("/order/42") ~> route ~> check {
handled shouldEqual false
rejections should not be empty
}
}
}
}