1
votes

I'm working on a Spray API, with an Akka router to send the incoming messages on to a pool of actors for handling the logic. Now I want to write some tests for the API, but I'm struggling to find the right structure for the code. The API looks as follows at the moment:

import akka.actor.{ActorRef, ActorSystem, Props, Actor}
import akka.io.IO
import akka.routing.SmallestMailboxPool
import akka.util.Timeout
import akka.pattern.ask
import com.typesafe.config.ConfigFactory
import spray.json._
import spray.can.Http
import scala.concurrent.duration._
import spray.routing._
import spray.http._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Success
import scala.util.Failure


object implicits{
  implicit val system = ActorSystem("ApiSystem")
  implicit val timeout = Timeout(5.seconds)
  implicit val conf = ConfigFactory.load()
  // Custom case class for parsing JSON parameter.
  case class Msg(key1:String, key2:String, key3:Int)

  object JsonProtocol extends DefaultJsonProtocol {
    implicit val msg = jsonFormat3(Msg)
  }
  case class PostMsg(msg:String)
  case object PostSuccess
  case class PostFailure(msg:String)
}

import implicits._

object MyApi extends App {
  override def main(Args: Array[String]):Unit = {

    // create and start our service actor
    val service = system.actorOf(Props(new MyApiActor(system)), "MyApi-service")


    IO(Http) ? Http.Bind(service, interface = conf.getString("http.host"), port = conf.getInt("http.port"))
  }
}

class MyApiActor(system: ActorSystem) extends Actor with MyApiService {
  // the HttpService trait defines only one abstract member, which
  // connects the services environment to the enclosing actor or test
  def actorRefFactory = context

  // this actor only runs our route, but you could add
  // other things here, like request stream processing
  // or timeout handling
  def receive = runRoute(myRoute)
}


// this trait defines our service behavior independently from the service actor
trait MyApiService extends HttpService {
  import implicits.JsonProtocol._

  var actorPool = system.actorOf(SmallestMailboxPool(conf.getInt("actor-number")).props(Props(new HandlingActor(conf))), "msgRouter")

  val myRoute =
    path("msg") {
      post {
        entity(as[String]) { obj =>
          try{
            // if this parsing succeeds, the posted msg satisfies the preconditions set.
            obj.parseJson.convertTo[Msg]
          } catch {
            case e: DeserializationException => {
              complete(HttpResponse(status=StatusCodes.BadRequest, entity="Invalid json provided."))
            }
            case e: Exception => {
              complete(HttpResponse(status=StatusCodes.InternalServerError, entity="Unknown internal server error."))
            }
          }
          onComplete(actorPool ? PostMsg(obj)) {
            case Success(value) => complete(HttpResponse(status = StatusCodes.OK, entity = "Pushed Msg"))
            case Failure(value) => complete(HttpResponse(status = StatusCodes.InternalServerError, entity = "Handling failed."))
          }
        }
      }
    }
}

What I would like to test is the response of the API to various HTTP messages (i.e. correct calls, incorrect calls etc.). The logic in the handling actor is simply to push the message to a Kafka bus, so I would like to "mock" this behaviour (i.e. be able to test the API response if this push succeeds and also what happens when this push fails).

The thing I'm struggling with most at the moment is how to setup the test. For now, I am setting up the API using the same commands as in the main method shown, but I need to specify a different actorPool, as I don't want any messages to actually be pushed. How should I best go about achieving such tests?

I am using Scalatest, with the Akka and Spray testkit. (plus possibly mockito for mocking if necessary)

1
Spray testkit lets you test your route directly without bringing up an actor system. Looks like you have to do it anyway, so you can use Akka testkit to control such actors and have a test actor system. Maybe some of the examples from here would help with route testing and trait composition: github.com/izmailoff/Spray_Mongo_REST_service/blob/master/rest/…yǝsʞǝla

1 Answers

3
votes

I have few suggestions to make your testing easier:

Do not create the actor pool in your trait. Instead inject the ActorRef from the ActorPool using a def instead of a val in the route. Then it will be easier to inject your actorPool TestProbe() to test. For example (I have not tried/compiled this code):

class MyApiActor extends Actor with MyApiService {
  // the HttpService trait defines only one abstract member, which
  // connects the services environment to the enclosing actor or test
  def actorRefFactory = context

  val actorPool = context.actorOf(SmallestMailboxPool(conf.getInt("actor-number")).props(Props(new HandlingActor(conf))), "msgRouter")

  // this actor only runs our route, but you could add
  // other things here, like request stream processing
  // or timeout handling
  def receive = runRoute(myRoute(actorPool))
}


// this trait defines our service behavior independently from the service actor
trait MyApiService extends HttpService {
  import implicits.JsonProtocol._

  def myRoute(actorPool: ActorRef) =
    path("msg") {
      post {
        entity(as[String]) { obj =>
          try{
            // if this parsing succeeds, the posted msg satisfies the preconditions set.
            obj.parseJson.convertTo[Msg]
          } catch {
            case e: DeserializationException => {
              complete(StatusCodes.BadRequest, "Invalid json provided.")
            }
            case e: Exception => {
              complete(StatusCodes.InternalServerError, "Unknown internal server error.")
            }
          }
          onComplete(actorPool ? PostMsg(obj)) {
            case Success(value) => complete(StatusCodes.OK, "Pushed Msg")
            case Failure(value) => complete(StatusCodes.InternalServerError, "Handling failed.")
          }
        }
      }
    }
}

Then the test can look like this:

class HttpListenerSpec extends WordSpecLike with Matchers with ScalatestRouteTest with MyApiService {

  "An HttpListener" should {
    "accept GET at /msg" in {
        val actorPool = TestProbe()

        (stuff for responding with TestProbe()...)

        Get("/msg") ~> myRoute(actorPool.ref) ~> check {
          status shouldBe OK
          val response = responseAs[String]
          assert(...)
        }
    }
  }
}

Also, as a final suggestion. There are implicit conversions that integrate spray json and spray so you can do entity(as[Msg]). Look for the following:

import spray.httpx.marshalling._
import spray.httpx.unmarshalling._
import spray.httpx.SprayJsonSupport._
import MsgJsonProtocol._