I came across this issue while I was working on a functional test for JAMLive!, a Play! application I'm currently working on obsessing over in my free time. I have a controller with a connect
method that looks like this:
/**
* Endpoint to ping when a user connects to the page and enters an id for
* himself/herself.
*/
def connect = Action { implicit request =>
val pId = playerForm.bindFromRequest.get
// Make this async so if there's ops on the players map the request
// won't block
val addPlayerFuture: Future[String] = Future[String] {
AudioPlayer.addPlayer(pId)
}
Async {
addPlayerFuture.map {res =>
res match {
case null => BadRequest(Json.obj(
"status" -> http.Status.BAD_REQUEST,
"error" -> "ID %s is already taken.".format(pId)
))
case _ => {
AudioPlayer.play(res)
Ok(Json.obj(
"status" -> http.Status.OK,
"playerId" -> res
))
}
}
}
}
}
As shown above, this action returns a play.api.mvc.AsyncResult
instead of a Result
. The reason for this is if there happens to be a large number of users connecting to the server at once, the routing controller should not have to block while waiting to update the AudioPlayer
's players
Map (which is an instance of ConcurrentMap
). You could say that this is overoptimization, but I call it planning ahead ;).
The problem now comes with testing this code. One of my test cases for this controller looks something like this:
"Application" should {
"send back an error if a player tries to connect with a taken id" in {
running(FakeApplication()) {
val _ = getConnectResponse
val attempt2 = getConnectResponse
val json = parse(contentAsString(attempt2))
(json \ "status").as[Int] must equalTo(BAD_REQUEST)
(json \ "error").as[String] must equalTo(s"""ID $playerId is already taken.""")
}
}
}
getConnectResponse
is a method I defined in my ApplicationSpec
companion object, which looks like:
/**
* Holds some fixtures and convenience methods for testing the application.
*
* @param playerId The player ID used for testing.
*/
object ApplicationSpec {
val playerId: String = "Bob"
/**
* Creates a request that would hypothetically be sent by a client back to the server when a user
* wanted to connect, and returns the reponse from that request.
*
* @return a Result instance representing that request.
*/
def getConnectResponse: Result = route(FakeRequest(POST, "/connect").withFormUrlEncodedBody(
("playerId" -> playerId)
)).get
}
The problem with this, as I'm sure you've noticed, is that this code introduces a race condition where the invoked getConnectResponse
call bound to attempt2
will execute successfully (i.e. not trigger a BAD_REQUEST
) if it happens to execute before the first getConnectRequest
, which will cause the test to fail. After slapping myself across the face for not having realized this, I immediately looked for a play.api.test.Helpers
method that would await completion of an AsyncResult
and return the result of its promise...and found nothing.
What wound up working for me was using the play.api.test.Helpers.await
method in conjunction with casting the result of route(...).get
to an AsyncResult
, which could then return a Future[Result]
that await
could be applied to. The final method definition looks like this:
def getConnectResponse: Result = await[Result](
route(FakeRequest(POST, "/connect").withFormUrlEncodedBody(
("playerId" -> playerId)
)).get.asInstanceOf[AsyncResult].result
)
It's a bit ugly, but it seems to do the trick. Anyways I hope that this comes in handy for anyone who's running into this issue while writing tests in play. Happy coding!! <3
AsyncResult is removed in Play 2.3.x, any idea how to make this same work in play 2.3.x ?