Akka Http: как протестировать маршрут с потоком на сторонний сервис?

У меня есть маршрут в приложении akka-http, который интегрирован со сторонней службой через Http().cachedHostConnectionPoolHttps. Я хочу правильно это проверить. Но не уверен, как должно быть :(

Вот как выглядит этот маршрут:

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"))
      }
    }
  }
}

За этим маршрутом скрывается accessTokenActor, где происходит вся логика, вот она:

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")
  }

  }

Итак, вопрос в том, как лучше протестировать этот маршрут, имея в виду, что актор с потоком также существует под его капотом.


person Alex Fruzenshtein    schedule 20.06.2017    source источник
comment
Создать в своем тесте фиктивный веб-сервер http и получить звонок?   -  person Diego Martinoia    schedule 20.06.2017
comment
@DiegoMartinoia да, это самый очевидный случай. Я сделаю это, если в этом случае больше ничего не сработает. Собственно ищу какую-нибудь технику для тестирования акка стримов. Что-то вроде подмены Flow на фейк ... Что вы думаете по этому поводу?   -  person Alex Fruzenshtein    schedule 20.06.2017
comment
Проблема с инъекциями зависимостей и имитацией заключается в том, что ваш тест не потерпит неудачу, если настоящая зависимость нарушена. Я меньшинство в этом вопросе, но особенно когда дело доходит до чего-то сложного, например удаленных HTTP-вызовов, мне нравится использовать черный ящик. В частности, в приложениях Akka обычно есть много странных вещей, которые вы хотите протестировать (постоянство, сегментирование, кластеризация), и мне легче развернуть приложение и проверить его на концах. но это я   -  person Diego Martinoia    schedule 20.06.2017


Ответы (1)


Композиция

Одна из трудностей с тестированием вашей логики маршрута в том виде, в котором она сейчас организована, заключается в том, что сложно изолировать функциональность. Невозможно протестировать вашу Route логику без Actor, и трудно протестировать ваш Актер, запрашивающий без Маршрута.

Я думаю, вам лучше подойдет композиция функций, так вы сможете изолировать то, что вы пытаетесь протестировать.

Сначала абстрагируйтесь от Actor запросов (спросите):

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]

Теперь преобразуйте значение routes в метод более высокого порядка, который принимает функцию запроса в качестве параметра:

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()

Тестирование

Теперь вы можете протестировать queryActorForToken без Route, и вы можете протестировать метод createRoute без участия актера!

Вы можете протестировать createRoute с помощью внедренной функции, которая всегда возвращает предварительно определенный токен:

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
}

Или вы можете протестировать createRoute с внедренной функцией, которая никогда не возвращает токен:

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

Get("/access-tokens/fooUser?refreshToken=bar" ~> alwaysFailsRoute ~> check {
  status shouldEqual StatusCodes.InternalServerError
  responseAs[String] shouldEqual errorMsg
}
person Ramón J Romero y Vigil    schedule 20.06.2017
comment
Классный подход :) В конце концов я понял, что моя ошибка - держать все вместе. Мне нужно больше независимых компонентов! - person Alex Fruzenshtein; 20.06.2017