16

I've just started testing out the Akka HTTP Request-Level Client-Side API (Future-Based). One thing I've been struggling to figure out is how to write a unit test for this. Is there a way to mock the response and have the future completed without having to actually do an HTTP request?

I was looking at the API and the testkit package, trying to see how I could use that, only to find in the docs that it actually says:

akka-http-testkit A test harness and set of utilities for verifying server-side service implementations

I was thinking something TestServer (kinda like the TestSource for Akka Streams) and use the server side routing DSL to create the expected response and somehow hook this up the the Http object.

Here is a simplified example of what the function does that I want to test:

object S3Bucket {

  def sampleTextFile(uri: Uri)(
    implicit akkaSystem: ActorSystem,
    akkaMaterializer: ActorMaterializer
  ): Future[String] = {
    val request = Http().singleRequest(HttpRequest(uri = uri))
    request.map { response => Unmarshal(response.entity).to[String] }
  }
}
steinybot
  • 5,491
  • 6
  • 37
  • 55
  • 1
    I never used It, but it looks like Spray testkit: https://github.com/theiterators/akka-http-microservice/blob/master/src/test/scala/ServiceSpec.scala. In Spray you don't have to bring up the Akka and can just test against the route directly (it's a PF). – yǝsʞǝla Jan 11 '16 at 06:04
  • Are you referring to `freeGeoIpConnectionFlow`? I think I'm missing something here. I can see that this is overriding the definition in [AkkaHttpMicroservice](https://github.com/theiterators/akka-http-microservice/blob/9ff6bdb67f9665817935ffe7107682e04056fa76/src/main/scala/AkkaHttpMicroservice.scala) but how does this get called in `ServiceSpec`? It seems like you need to call `AkkaHttpMicroservice.apply()` to get the bindings. – steinybot Jan 11 '16 at 06:28
  • 2
    There are 2 ways to test REST API in Spray/AkkaHttp: 1) startup actor system as you would run the whole application, test it with http client, shut it down; 2) test against routing DSL which is essentially a PF and does not require actor system to run. I'm after the second option because it's more lightweight and more like unit test vs integration test (1). In this case we would not have to bind to the network interface and no actors need to be started to handle the route, unless you use actors somewhere else. I never tried this on AkkaHttp, speaking from Spray experience. – yǝsʞǝla Jan 11 '16 at 06:43
  • This is the Spray doc: http://spray.io/documentation/1.2.2/spray-testkit/. "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." – yǝsʞǝla Jan 11 '16 at 06:44
  • 1
    I think this has the same focus as the akka-http-testkit. It is for testing the routing not for testing the client. – steinybot Jan 11 '16 at 22:47
  • If you want to test the client only then you don't need any of the akka-http* libraries. Just pass completed Future to your code and do regular unit test. – yǝsʞǝla Jan 11 '16 at 23:57
  • I have added an example of the function I want to test. What is the best way to just just pass the completed Future to this? – steinybot Jan 13 '16 at 00:19

3 Answers3

9

I think in general terms you've already hit on the fact that the best approach is to mock the response. In Scala, this can be done using Scala Mock http://scalamock.org/

If you arrange your code so that your instance of akka.http.scaladsl.HttpExt is dependency injected into the code which uses it (e.g. as a constructor parameter), then during testing you can inject an instance of mock[HttpExt] rather than one built using the Http apply method.

EDIT: I guess this was voted down for not being specific enough. Here is how I would structure the mocking of your scenario. It is made a little more complicated by all the implicitis.

Code in main:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{Uri, HttpResponse, HttpRequest}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer

import scala.concurrent.{ExecutionContext, Future}

trait S3BucketTrait {

  type HttpResponder = HttpRequest => Future[HttpResponse]

  def responder: HttpResponder

  implicit def actorSystem: ActorSystem

  implicit def actorMaterializer: ActorMaterializer

  implicit def ec: ExecutionContext

  def sampleTextFile(uri: Uri): Future[String] = {

    val responseF = responder(HttpRequest(uri = uri))
    responseF.flatMap { response => Unmarshal(response.entity).to[String] }
  }
}

class S3Bucket(implicit val actorSystem: ActorSystem, val actorMaterializer: ActorMaterializer) extends S3BucketTrait {

  override val ec: ExecutionContext = actorSystem.dispatcher

  override def responder = Http().singleRequest(_)
}

Code in test:

import akka.actor.ActorSystem
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import akka.testkit.TestKit
import org.scalatest.{BeforeAndAfterAll, WordSpecLike, Matchers}
import org.scalamock.scalatest.MockFactory
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.Future

class S3BucketSpec extends TestKit(ActorSystem("S3BucketSpec"))
with WordSpecLike with Matchers with MockFactory with BeforeAndAfterAll  {


  class MockS3Bucket(reqRespPairs: Seq[(Uri, String)]) extends S3BucketTrait{

    override implicit val actorSystem = system

    override implicit val ec = actorSystem.dispatcher

    override implicit val actorMaterializer = ActorMaterializer()(system)

    val mock = mockFunction[HttpRequest, Future[HttpResponse]]

    override val responder: HttpResponder = mock

    reqRespPairs.foreach{
      case (uri, respString) =>
        val req = HttpRequest(HttpMethods.GET, uri)
        val resp = HttpResponse(status = StatusCodes.OK, entity = respString)
        mock.expects(req).returning(Future.successful(resp))
    }
  }

  "S3Bucket" should {

    "Marshall responses to Strings" in {
      val mock = new MockS3Bucket(Seq((Uri("http://example.com/1"), "Response 1"), (Uri("http://example.com/2"), "Response 2")))
      Await.result(mock.sampleTextFile("http://example.com/1"), 1 second) should be ("Response 1")
      Await.result(mock.sampleTextFile("http://example.com/2"), 1 second) should be ("Response 2")
    }
  }

  override def afterAll(): Unit = {
    val termination = system.terminate()
    Await.ready(termination, Duration.Inf)
  }
}

build.sbt dependencies:

libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.0.1"

libraryDependencies += "org.scalamock" %% "scalamock-scalatest-support" % "3.2" % "test"

libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.6"

libraryDependencies += "com.typesafe.akka" % "akka-testkit_2.11" % "2.4.1"
mattinbits
  • 10,370
  • 1
  • 26
  • 35
  • I have added an example. – steinybot Jan 13 '16 at 00:19
  • There are a couple of problems with this way as opposed to my one. The `S3BucketTrait` trait can't be mixed into other types that need to mock the response since it has the `sampleTextFile` and unmarshalled type baked into it. Also it seems like a code smell to be testing a mock object even if the function we are calling isn't being overridden. It seems like the only option is to have the `HttpResponder` as a parameter to the function or use the cake pattern. – steinybot Jan 14 '16 at 22:17
  • 2
    Fair enough, however I was not trying to completely rewrite your original example to a perfect case, merely make the necessary to changes to show a way of using ScalaMock to answer your question of "Is there a way to mock the response and have the future completed without having to actually do an HTTP request?" If you go with the cake pattern, you may still use ScalaMock in a similar way to provide an implementation of `HttpResponder` – mattinbits Jan 15 '16 at 22:11
4

Considering that you indeed want to write a unit test for your HTTP client you should pretend there is no real server and not cross the network boundary, otherwise you will obviously do integration tests. A long known recipe of enforcing a unit-testable separation in such cases as yours is to split interface and implementation. Just define an interface abstracting access to an external HTTP server and its real and fake implementations as in the following sketch

import akka.actor.Actor
import akka.pattern.pipe
import akka.http.scaladsl.HttpExt
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, StatusCodes}
import scala.concurrent.Future

trait HTTPServer {
  def sendRequest: Future[HttpResponse]
}

class FakeServer extends HTTPServer {
  override def sendRequest: Future[HttpResponse] =
    Future.successful(HttpResponse(StatusCodes.OK))
}

class RealServer extends HTTPServer {

  def http: HttpExt = ??? //can be passed as a constructor parameter for example

  override def sendRequest: Future[HttpResponse] =
    http.singleRequest(HttpRequest(???))
}

class HTTPClientActor(httpServer: HTTPServer) extends Actor {

  override def preStart(): Unit = {
    import context.dispatcher
    httpServer.sendRequest pipeTo self
  }

  override def receive: Receive = ???
}

and test your HTTPClientActor in conjunction with FakeServer.

Max Plevako
  • 1,832
  • 11
  • 11
  • Is it possible to unit-test a function that uses Akka Streams (like consuming the HTTP response body) without running an ActorSystem? It looks quite easy to mock the request method and substitute the response object, but consuming the response body requires a materializer, which looks dependent of ActorSystem – Konstantin Pelepelin Jul 25 '16 at 17:53
2

I was hoping there might be a way to leverage some sort of test actor system but in the absence of that (or some other idiomatic way) I am probably going to do something like this:

object S3Bucket {

  type HttpResponder = HttpRequest => Future[HttpResponse]

  def defaultResponder = Http().singleRequest(_)

  def sampleTextFile(uri: Uri)(
    implicit akkaSystem: ActorSystem,
    akkaMaterializer: ActorMaterializer,
    responder: HttpResponder = defaultResponder
  ): Future[String] = {
    val request = responder(HttpRequest(uri = uri))
    request.map { response => Unmarshal(response.entity).to[String] }
  }
}

Then in my test I can just provide a mock HttpResponder.

steinybot
  • 5,491
  • 6
  • 37
  • 55
  • I will probably pull out the type alias and default into a trait that can be mixed into other objects that do HTTP requests. Perhaps also adding something similar for the Flow variant too. – steinybot Jan 14 '16 at 22:10