2

I have a route in akka-http app which is integrated with a 3rd party service via Http().cachedHostConnectionPoolHttps. I want to test it in a right way. But not sure how it should be :(

Here is how this route looks like:

val routes: Route = pathPrefix("access-tokens") {
  pathPrefix(Segment) { userId =>
    parameters('refreshToken) { refreshToken =>
      onSuccess(accessTokenActor ? GetAccessToken(userId, refreshToken)) {
        case token: AccessToken => complete(ok(token.toJson))
        case AccessTokenError => complete(internalServerError("There was problems while retriving the access token"))
      }
    }
  }
}

Behind this route hides accessTokenActor where all logic happens, here it is:

class AccessTokenActor extends Actor with ActorLogging with APIConfig {

  implicit val actorSystem = context.system
  import context.dispatcher
  implicit val materializer = ActorMaterializer()

  import AccessTokenActor._

  val connectionFlow = Http().cachedHostConnectionPoolHttps[String]("www.service.token.provider.com")

  override def receive: Receive = {
    case get: GetAccessToken => {
      val senderActor = sender()
        Source.fromFuture(Future.successful(
          HttpRequest(
            HttpMethods.GET,
            "/oauth2/token",
            Nil,
            FormData(Map(
              "clientId" -> youtubeClientId,"clientSecret" -> youtubeSecret,"refreshToken" -> get.refreshToken))
              .toEntity(HttpCharsets.`UTF-8`)) -> get.channelId
          )
        )
        .via(connectionFlow)
        .map {
          case (Success(resp), id) => resp.status match {
            case StatusCodes.OK => Unmarshal(resp.entity).to[AccessTokenModel]
              .map(senderActor ! AccessToken(_.access_token))
            case _ => senderActor ! AccessTokenError
          }
          case _ => senderActor ! AccessTokenError
        }
    }.runWith(Sink.head)
    case _ => log.info("Unknown message")
  }

  }

So the question is how it's better to test this route, keeping in mind that the actor with the stream also exist under its hood.

Alex Fruzenshtein
  • 2,846
  • 6
  • 32
  • 53
  • Instantiate a mock web http server in your test and receive the call? – Diego Martinoia Jun 20 '17 at 14:43
  • @DiegoMartinoia yes, this is the most obvious case. I'm going to do it if nothing else would work in this case. Actually I'm looking for some testing technics for akka streams. Something like Flow substitution with fake... What do you think about this? – Alex Fruzenshtein Jun 20 '17 at 14:49
  • The thing with dependency injections and mock is that your test won't fail if the "real" dependency is broken. I'm a minority on this, but especially when it comes to something complicated like remote http calls, I like to black box things. Akka apps in particular usually ahve a lot of "weird" stuff that you want to test (persistence, sharding, clustering) that I find it easier to actually spin the app and probe it at the ends. but that's me – Diego Martinoia Jun 20 '17 at 15:13

1 Answers1

4

Composition

One difficulty with testing your route logic, as currently organized, is that it is hard to isolate functionality. It is impossible to test your Route logic without an Actor, and it is hard to test your Actor querying without a Route.

I think you would be better served with function composition, that way you can isolate what it is you're trying to test.

First abstract away the Actor querying (ask):

sealed trait TokenResponse
case class AccessToken() extends TokenResponse {...} 
case object AccessTokenError extends TokenResponse

val queryActorForToken : (ActorRef) => (GetAccessToken) => Future[TokenResponse] = 
  (ref) => (getAccessToken) => (ref ? getAccessToken).mapTo[TokenResponse]

Now convert your routes value into a higher-order method which takes in the query function as a parameter:

val actorRef : ActorRef = ??? //not shown in question

type TokenQuery = GetAccessToken => Future[TokenResponse]

val actorTokenQuery : TokenQuery = queryActorForToken(actorRef)

val errorMsg = "There was problems while retriving the access token"

def createRoute(getToken : TokenQuery = actorTokenQuery) : Route = 
  pathPrefix("access-tokens") {
    pathPrefix(Segment) { userId =>
      parameters('refreshToken) { refreshToken =>
        onSuccess(getToken(GetAccessToken(userId, refreshToken))) {
          case token: AccessToken => complete(ok(token.toJson))
          case AccessTokenError   => complete(internalServerError(errorMsg))
        }
      }
    }
  }

//original routes
val routes = createRoute()

Testing

Now you can test queryActorForToken without needing a Route and you can test the createRoute method without needing an actor!

You can test createRoute with an injected function that always returns a pre-defined token:

val testToken : AccessToken = ???

val alwaysSuccceedsRoute = createRoute(_ => Success(testToken))

Get("/access-tokens/fooUser?refreshToken=bar" ~> alwaysSucceedsRoute ~> check {
  status shouldEqual StatusCodes.Ok
  responseAs[String] shouldEqual testToken.toJson
}

Or, you can test createRoute with an injected function that never returns a token:

val alwaysFailsRoute = createRoute(_ => Success(AccessTokenError))

Get("/access-tokens/fooUser?refreshToken=bar" ~> alwaysFailsRoute ~> check {
  status shouldEqual StatusCodes.InternalServerError
  responseAs[String] shouldEqual errorMsg
}
Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
  • Awesome approach :) To the end of the day I understood that my mistake is to keep everything together. I need to have more independent components! – Alex Fruzenshtein Jun 20 '17 at 19:18