Skip to content
Mathias edited this page Jul 26, 2011 · 6 revisions

One of sprays core design goals is optimal testability of the created web services. Since actor-based systems can sometimes be cumbersome to test spray fosters the separation of route logic from actor code. If you design your web service routes with sprays internal routing DSL your service will be fully testable without the need to fire up actors or any type of container.

spray-server comes with the SprayTest trait that enables convenient route and service testing using testing frameworks like scalatest, specs or [specs2][].

Here is an example of what spray tests can look like:

package cc.spray.examples.calculator

import org.specs2.mutable._
import cc.spray._
import test._
import http._
import HttpMethods._
import HttpHeaders._
import MediaTypes._
import StatusCodes._

class CalculatorServiceSpec extends Specification with SprayTest with CalculatorService with DontDetach {

  "The Calculator service" should {
    "calculate correctly" in {
      "for 'add's" in {
        testService(HttpRequest(GET, "/add/35/7.2")) {
          calculatorService
        }.response.content.as[String] mustEqual Right("<double>42.2</double>")
      }
      "for 'substract's" in {
        testService(HttpRequest(GET, "/substract/35.1/7", headers = List(`Accept`(`text/plain`)))) {
          calculatorService
        }.response.content.as[String] mustEqual Right("28.1")
      }
    }
    "use the given onDivZero parameter value on division by zero" in {
      testService(HttpRequest(GET, "/divide/12/0?onDivZero=nah!")) {
        calculatorService
      }.response.content.as[String] mustEqual Right("nah!")
    }
    "return a MethodNotAllowed error for POST requests to '/substract/123/234'" in {
      testService(HttpRequest(POST, "/substract/123/234")) {
        calculatorService
      }.response mustEqual HttpResponse(MethodNotAllowed, "HTTP method not allowed, supported methods: GET")
    }
    "leave GET requests to other paths unhandled" in {
      testService(HttpRequest(GET, "/multiply/x/22")) {
        calculatorService
      }.handled must beFalse
    }
  }
}

This example uses [specs2][] to test a calculator service (actually it is an excerpt from the calculator example, see Example Projects). You are free to choose any testing framework you like, the only requirement is that the type you mix the SprayTest trait into must define a fail(msg: String): Nothing or failure(msg: String): Nothing method (which scalatest, specs and [specs2][] do).

The SprayTest trait defines two main test methods: test and testService. The first is used for route testing while the second can also test the HttpService actor logic you might have customized.

The test Method

This is how you test a route with the test method:

val result = test(HttpRequest(...)) { 
  route
}

The test method takes as parameter an HttpRequest object that it "shoots" into your route. There are three possible outcomes:

  • The request is completed
  • The request is rejected
  • The request is not responded to at all (i.e. the route did not call complete or reject on the RequestContext)

The last case is usually an error, so spray automatically verifies that your route either completes or rejects the request. Normally you simply define assertions on result.response (which is an HttpResponse object) or on result.rejections (which is a set of Rejections).

The testService Method

If you do not want to specify tests on the Rejection level but rather on the very outside of your service, where rejections have already been translated into HttpResponses you use the testService method. Just like in the case of the test method you usually define assertions on the response member of the object returned by testService.

If you have derived your own CustomHttpServiceLogic that you would like to test, (see the Custom Error Responses chapter for more on this) create an implicit conversion similar to this:

implicit def customWrapRootRoute(rootRoute: Route): ServiceTest = {
  new CustomHttpServiceLogic with ServiceTest {
    val route = routeRoute
  }
}

testService will use your implicit conversion (if it's in scope) and allow you to test your own custom rejection handling code.

The DontDetach Trait

sprays detach directive interferes with proper testing since it decouples the route execution from the thread the testing logic is running in, causing your route to appear to not respond to the request. You should therefore disable detaching in your tests by mixing in the DontDetach trait into the class or trait building your service. All that this trait does is overriding the detach directive to not actually detach.

Clone this wiki locally