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
}
}
}
}