2016-01-11 18 views
12

Ho appena iniziato a testare lo Akka HTTP Request-Level Client-Side API (Future-Based). Una cosa che ho cercato di capire è come scrivere un test unitario per questo. C'è un modo per deridere la risposta e avere il futuro completato senza dover effettivamente fare una richiesta HTTP?Come testare Akka HTTP lato client

stavo guardando l'API e il pacchetto TESTKIT, cercando di vedere come potevo usare che, solo per trovare nella documentazione che in realtà dice:

Akka-http-TESTKIT Un test imbracatura e una serie di utility per verificare le implementazioni di servizi lato server

stavo pensando qualcosa TestServer (un po 'come la TestSource per Akka Streams) e utilizzare il DSL lato server di routing per creare la risposta attesa e in qualche modo agganciano questo il il Http oggetto.

Ecco un esempio semplificato di ciò che fa la funzione che voglio prova:

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] } 
    } 
} 
+1

Non l'ho mai usato, ma sembra Spray testkit: https://github.com/theiterators/akka-http-microservice/blob/master/src/test/scala/ServiceSpec.scala. In Spray non devi far apparire l'Akka e puoi semplicemente provare contro il percorso direttamente (è un PF). –

+0

Ti riferisci a 'freeGeoIpConnectionFlow'? Penso che mi manchi qualcosa qui. Posso vedere che questo è prevalente sulla definizione in [AkkaHttpMicroservice] (https://github.com/theiterators/akka-http-microservice/blob/9ff6bdb67f9665817935ffe7107682e04056fa76/src/main/scala/AkkaHttpMicroservice.scala) ma come viene chiamato questo? in 'ServiceSpec'? Sembra che tu abbia bisogno di chiamare 'AkkaHttpMicroservice.apply()' per ottenere i binding. – Steiny

+1

Esistono 2 modi per testare l'API REST in Spray/AkkaHttp: 1) sistema di attore di avvio come si eseguirà l'intera applicazione, testandola con il client http, spegnendola; 2) test contro il routing DSL che è essenzialmente un PF e non richiede l'esecuzione del sistema degli attori. Sono in cerca della seconda opzione perché è più leggera e più simile al test unit test e all'integrazione (1). In questo caso non dovremmo legarci all'interfaccia di rete e nessun attore deve essere avviato per gestire il percorso, a meno che tu non usi gli attori da qualche altra parte. Non ho mai provato questo su AkkaHttp, parlando dall'esperienza Spray. –

risposta

4

Penso che in termini generali che hai già colpito sul fatto che l'approccio migliore è quello di prendere in giro la risposta. In Scala, questo può essere fatto utilizzando Scala Mock http://scalamock.org/

Se ad organizzare il codice in modo che l'istanza di akka.http.scaladsl.HttpExt è la dipendenza iniettato nel codice che lo utilizza (ad esempio come parametro del costruttore), quindi durante il test si può iniettare un istanza di mock[HttpExt] invece di una creata utilizzando il metodo di applicazione Http.

EDIT: Immagino che questo sia stato votato per non essere abbastanza specifico. Ecco come strutturerei il beffardo del tuo scenario. È reso un po 'più complicato da tutti gli impliciti.

Codice 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(_) 
} 

Codice 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 dipendenze:

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

Ho aggiunto un esempio. – Steiny

+0

Ci sono un paio di problemi in questo modo rispetto al mio. La caratteristica 'S3BucketTrait' non può essere mescolata in altri tipi che hanno bisogno di prendere in giro la risposta dato che ha il' sampleTextFile' e il tipo unmarshalled cotto in esso. Sembra anche un odore di codice testare un oggetto finto anche se la funzione che stiamo chiamando non viene sovrascritta. Sembra che l'unica opzione sia quella di avere il 'HttpResponder' come parametro per la funzione o usare il modello di torta. – Steiny

+1

Abbastanza corretto, tuttavia non stavo cercando di riscrivere completamente il tuo esempio originale in un caso perfetto, ma semplicemente di apportare le modifiche necessarie per mostrare un modo di usare ScalaMock per rispondere alla tua domanda "C'è un modo per deridere la risposta e avere il futuro completato senza dover effettivamente fare una richiesta HTTP? " Se segui lo schema di torta, puoi comunque usare ScalaMock in modo simile per fornire un'implementazione di 'HttpResponder' – mattinbits

4

Considerando che, invece, si desidera scrivere uno unit test per il vostro client HTTP dovresti fingere che non ci sia r eal server e non oltrepassare il confine della rete, altrimenti farai ovviamente dei test di integrazione. Una ricetta nota da lungo tempo per applicare una separazione testabile per unità in questi casi è dividere l'interfaccia e l'implementazione. Basta definire un'interfaccia astrarre l'accesso a un server HTTP esterni e le sue implementazioni reali e falsi come nel seguente schizzo

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 = ??? 
} 

e testare il HTTPClientActor in collaborazione con FakeServer.

+0

E 'possibile testare unitamente una funzione che usa i flussi di Akka (come consumare il corpo della risposta HTTP) senza eseguire un ActorSystem? Sembra abbastanza facile prendere in giro il metodo di richiesta e sostituire l'oggetto risposta, ma consumare il corpo della risposta richiede un materializzatore, che sembra dipendente da ActorSystem –

1

Speravo ci potrebbe essere un modo per sfruttare una sorta di sistema di test attore, ma in assenza di quella (o qualche altro modo idiomatica) probabilmente sto andando a fare qualcosa di simile:

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] } 
    } 
} 

Poi nel mio test posso solo fornire un simulato HttpResponder.

+0

Probabilmente estrarrò il tipo alias e default in una caratteristica che può essere mescolata in altri oggetti che fanno richieste HTTP. Forse aggiungendo anche qualcosa di simile per la variante Flow. – Steiny