How to test client-side Akka HTTP

前端 未结 3 1559
挽巷
挽巷 2021-02-07 09:55

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

相关标签:
3条回答
  • 2021-02-07 10:45

    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"
    
    0 讨论(0)
  • 2021-02-07 10:46

    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.

    0 讨论(0)
  • 2021-02-07 10:53

    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.

    0 讨论(0)
提交回复
热议问题