Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save traviskaufman/5210421 to your computer and use it in GitHub Desktop.
Save traviskaufman/5210421 to your computer and use it in GitHub Desktop.
Testing Asynchronous HTTP Responses in Play Framework

Testing Asynchronous Responses in Play! Framework

TL;DR

The Background

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

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.

The Solution

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

@mzafer
Copy link

mzafer commented Jul 18, 2014

AsyncResult is removed in Play 2.3.x, any idea how to make this same work in play 2.3.x ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment