Why does this akka-http route test never complete

2019-08-26 02:03发布

问题:

I've a simple route and some tests that success individually, but collectively fail with timeout. Any idea why?

val route = (requestHandler: ActorRef @@ Web) => {
   get {
     pathPrefix("apps") {
       pathEndOrSingleSlash {
         completeWith(implicitly[ToEntityMarshaller[List[String]]]) { callback =>
           requestHandler ! GetAppsRequest(callback)
         }
       } ~ path("stats") {
         completeWith(implicitly[ToEntityMarshaller[List[Stats]]]) { callback =>
           requestHandler ! GetStatsRequest(callback)
         }
       }
     } ~ path("apps" / Segment / "stats") { app =>
       completeWith(implicitly[ToEntityMarshaller[Stats]]) { callback =>
         requestHandler ! GetStatsForOneRequest(app, callback)
       }
     }
   }
 }

and tests:

val testProbe = TestProbe()
val testProbeActor = testProbe.ref
  .taggedWith[Web]

val timeout = 1.minute

"Route" should "respond to get apps request" in {
  implicit val routeTestTimout = RouteTestTimeout(timeout.dilated)
  Get("/apps") ~> route(testProbeActor) ~> check {

    testProbe.receiveOne(timeout) match {
      case GetAppsRequest(callback) => {
        callback(k8SProperties.apps)
      }
    }
    entityAs[List[String]] should contain("test")
  }
  testProbe.expectNoMessage(timeout)
}

it should "respond to get stats request for all apps" in {
  implicit val routeTestTimout = RouteTestTimeout(timeout.dilated)
  val app = "test"
  Get("/apps/stats") ~> route(testProbeActor) ~> check {

    testProbe.receiveOne(timeout) match {
      case GetStatsRequest(callback) => {
        callback(List(Stats(app, ChronoUnit.SECONDS, Nil)))
      }
      case other => fail(s"Unexpected message $other.")
    }
    entityAs[List[Stats]].size shouldBe (1)
    entityAs[List[Stats]].head.app shouldBe (app)
  }
  testProbe.expectNoMessage(timeout)
}

it should "respond to get stats request for one app" in {
  implicit val routeTestTimout = RouteTestTimeout(timeout.dilated)
  val app = "test"
  Get(s"/apps/$app/stats") ~> route(testProbeActor) ~> check {

    testProbe.receiveOne(timeout) match {
      case GetStatsForOneRequest(app, callback) => {
        callback(Stats(app, ChronoUnit.SECONDS, Nil))
      }
      case other => fail(s"Unexpected message $other.")
    }
    entityAs[Stats].app shouldBe (app)
  }
  testProbe.expectNoMessage(timeout)
}

Edit: Opened https://github.com/akka/akka-http/issues/1615

回答1:

The problem is that you are using a single TestProbe across all three tests. That TestProbe is a single actor and is therefore receiving messages from all three tests. If you simply move your test probe creation and config inside the test bodies, it should work as you expect; specifically these two lines:

val testProbe = TestProbe()
val testProbeActor = testProbe.ref
  .taggedWith[Web]


回答2:

Working code, thanks to me.

"Routes" should "respond to get apps request" in {
  testProbe.setAutoPilot((_: ActorRef, msg: Any) => {
    msg match {
      case GetAppsRequest(callback) => callback(List("test")); TestActor.KeepRunning
      case _ => TestActor.NoAutoPilot
    }
  })

  Get("/apps") ~> handler ~> check {
    entityAs[List[String]] should contain("test")
  }
}

Putting TestProbe inside check hangs forever probably because it creates a deadlock; test waits for callback to be called, and callback isn't called until the body is executed.

Using autopilot sets up an "expectation" that can be fulfilled later thus avoiding the deadlock.