spray-testkit
One of sprays core design goals is good testability of the created services. Since actor-based systems can sometimes be cumbersome to test spray fosters the separation of processing logic from actor code in most of its modules.
For services built with spray-routing spray provides a dedicated test DSL that makes actor-less testing of route logic easy and convenient. This “route test DSL” is made available with the spray-testkit module.
Dependencies
Apart from the Scala library (see Current Versions chapter) spray-testkit depends on
- spray-http (with ‘provided’ scope)
- spray-httpx (with ‘provided’ scope)
- spray-routing (with ‘provided’ scope)
- spray-util
- akka-actor 2.2.x (with ‘provided’ scope, i.e. you need to pull it in yourself)
- akka-testkit 2.2.x (with ‘provided’ scope, i.e. you need to pull it in yourself)
- scalatest (with ‘provided’ scope, for the
ScalatestRouteTest
) - specs2 (with ‘provided’ scope, for the
Specs2RouteTest
)
Installation
The Maven Repository chapter contains all the info about how to pull spray-testkit into your classpath.
However, since you normally don’t need to have access to spray-testkit from your production code, you should limit
the dependency to the test
scope:
libraryDependencies += "io.spray" % "spray-testkit" % version % "test"
Currently spray-testkit supports the two most popular scala testing frameworks, scalatest and specs2. Depending on
which one you are using you need to mix either the ScalatestRouteTest
or the Specs2RouteTest
trait into your
test specification.
Usage
Here is an example of what a simple test with spray-testkit might look like:
import org.specs2.mutable.Specification
import spray.testkit.Specs2RouteTest
import spray.routing.HttpService
import spray.http.StatusCodes._
class FullTestKitExampleSpec extends Specification with Specs2RouteTest with HttpService {
def actorRefFactory = system // connect the DSL to the test ActorSystem
val smallRoute =
get {
pathSingleSlash {
complete {
<html>
<body>
<h1>Say hello to <i>spray</i>!</h1>
</body>
</html>
}
} ~
path("ping") {
complete("PONG!")
}
}
"The service" should {
"return a greeting for GET requests to the root path" in {
Get() ~> smallRoute ~> check {
responseAs[String] must contain("Say hello")
}
}
"return a 'PONG!' response for GET requests to /ping" in {
Get("/ping") ~> smallRoute ~> check {
responseAs[String] === "PONG!"
}
}
"leave GET requests to other paths unhandled" in {
Get("/kermit") ~> smallRoute ~> check {
handled must beFalse
}
}
"return a MethodNotAllowed error for PUT requests to the root path" in {
Put() ~> sealRoute(smallRoute) ~> check {
status === MethodNotAllowed
responseAs[String] === "HTTP method not allowed, supported methods: GET"
}
}
}
}
The basic structure of a test built with spray-testkit is this (expression placeholder in all-caps):
REQUEST ~> ROUTE ~> check {
ASSERTIONS
}
In this template REQUEST is an expression evaluating to an HttpRequest
instance. Since both RouteTest traits
extend the spray-httpx Request Building trait you have access to its mini-DSL for convenient and concise request
construction. [1]
ROUTE is an expression evaluating to a spray-routing Route
. You can specify one inline or simply refer to the
route structure defined in your service.
The final element of the ~>
chain is a check
call, which takes a block of assertions as parameter. In this block
you define your requirements onto the result produced by your route after having processed the given request. Typically
you use one of the defined “inspectors” to retrieve a particular element of the routes response and express assertions
against it using the test DSL provided by your test framework. For example, with specs2, in order to verify that your
route responds to the request with a status 200 response, you’d use the status
inspector and express an assertion
like this:
status mustEqual 200
The following inspectors are defined:
Inspector | Description |
---|---|
body: HttpEntity.NonEmpty |
Returns the contents of the response entity. If the response entity is empty a test failure is triggered. |
charset: HttpCharset |
Identical to contentType.charset |
chunks: List[MessageChunk] |
Returns the list of message chunks produced by the route. |
closingExtension: String |
Returns chunk extensions the route produced with a
ChunkedMessageEnd response part. |
contentType: ContentType |
Identical to body.contentType |
definedCharset: Option[HttpCharset] |
Identical to contentType.definedCharset |
entity: HttpEntity |
Identical to response.entity |
handled: Boolean |
Indicates whether the route produced an HttpResponse for the
request. If the route rejected the request handled evaluates to
false . |
header(name: String): Option[HttpHeader] |
Returns the response header with the given name or None if no such
header can be found. |
header[T <: HttpHeader: ClassTag]: Option[T] |
Identical to response.header[T] |
headers: List[HttpHeader] |
Identical to response.headers |
mediaType: MediaType |
Identical to contentType.mediaType |
rejection: Rejection |
The rejection produced by the route. If the route did not produce exactly one rejection a test failure is triggered. |
rejections: List[Rejection] |
The rejections produced by the route. If the route did not reject the request a test failure is triggered. |
response: HttpResponse |
The HttpResponse returned by the route. If the route did not return
an HttpResponse instance (e.g. because it rejected the request) a
test failure is triggered. |
responseAs[T: Unmarshaller: ClassTag]: T |
Unmarshals the response entity using the in-scope
FromResponseUnmarshaller for the given type. Any errors in the
process trigger a test failure. |
status: StatusCode |
Identical to response.status |
trailer: List[HttpHeader] |
Returns the list of trailer headers the route produced with a
ChunkedMessageEnd response part. |
[1] | If the request URI is relative it will be made absolute using an implicitly available instance of
DefaultHostInfo whose value is “http://example.com” by default. This mirrors the behavior of spray-can
which always produces absolute URIs for incoming request based on the request URI and the Host -header of
the request. You can customize this behavior by bringing an instance of DefaultHostInfo into scope. |
Sealing Routes
The section above describes how to test a “regular” branch of your route structure, which reacts to incoming requests with HTTP response parts or rejections. Sometimes, however, you will want to verify that your service also translates Rejections to HTTP responses in the way you expect.
You do this by wrapping your route with the sealRoute
method defined by the HttpService
trait.
The sealRoute
wrapper applies the logic of the in-scope ExceptionHandler and
RejectionHandler to all exceptions and rejections coming back from the route, and translates them
to the respective HttpResponse
.
The on-spray-can examples defines a simple test using sealRoute
like this:
"return a MethodNotAllowed error for PUT requests to the root path" in {
Put() ~> sealRoute(demoRoute) ~> check {
status === MethodNotAllowed
responseAs[String] === "HTTP method not allowed, supported methods: GET, POST"
}
}
Examples
A full example of how an API service definition can be structured in order to be testable with spray-testkit and without actor involvement is shown with the on-spray-can example. This is its test definition.
Another great pool of examples are the tests for all the predefined directives in spray-routing. They can be found here.