play2: FakeRequest().withBody(body) автоматически преобразуется в Request[AnyContentAsEmpty] в контроллере

Я работаю над проектом play-2.4 и написал такой контроллер:

package controllers

import play.api._
import play.api.mvc._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class Application extends Controller {
  def index = Action.async { implicit request =>
    Future { Ok(request.body.asJson.get) }
  }
}

с POST / controllers.Application.index в conf/routes.

Я проверил, что это работает нормально, выполнив curl --request POST --header "Content-type: application/json" --data '{"foo":"bar"}' http://localhost:9000/.

Теперь я написал спецификацию для этого контроллера:

package controllers

import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._

import play.api.test._
import play.api.test.Helpers._

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
  "Application" should {
    val controller = new Application
    val fakeJson = """{ "foo":"bar" }"""
    val fakeRequest = FakeRequest()
      .withHeaders("Content-type" -> "application/json")
      .withBody(fakeJson)
    val index = controller.index()(fakeRequest).run
    status(index) must equalTo(OK)
  }
}

но это привело к ошибке времени выполнения:

[error]    None.get (Application.scala:11)
[error] controllers.Application$$anonfun$index$1$$anonfun$apply$1.apply(Application.scala:11)
[error] controllers.Application$$anonfun$index$1$$anonfun$apply$1.apply(Application.scala:11)

Я вставил println(request.body) в контроллер и обнаружил, что тело запроса было AnyContentAsEmpty, что означает, что fakeJson было удалено из fakeRequest.

Как я могу правильно прикрепить JSON к FakeRequest?

* примечание: хотя я могу написать как FakeRequest(POST, '/', FakeHeaders(), fakeJson), но я думаю, что это нехорошо, потому что спецификация контроллера не должна обрабатывать методы или маршруты HTTP.

Буду признателен за любую помощь.


person iTakeshi    schedule 14.02.2016    source источник


Ответы (2)


Если клиент выполняет HTTP POST для вашего действия с запросом, который не является JSON, request.body.asJson.get выдаст исключение.

  1. body.asJson имеет тип возврата Option[JsValue] и возвращает None, если запрос не был JSON.
  2. При вызове get на None выдается java.util.NoSuchElementException.
  3. Это исключение проявляется в том, что Play возвращает 500 Internal Server Error.

Вы должны заменить def index = Action.async ... на действие, которое вместо этого использует JSON парсер тела:

import play.api.mvc.BodyParsers.parse

def index = Action.async(parse.json) ...

Это достигается несколькими вещами:

  1. Это более самодокументируемо (действие говорит «Я ожидаю JSON» прямо в объявлении метода).
  2. Play сгенерирует 400 Bad Request, если POST не был JSON. Это более уместно, чем 500 Internal Server Error, вызванное вашим None.get.
  3. Это сделает request.body JsValue вместо AnyContent. Таким образом, вы можете заменить request.body.asJson.get просто на request.body. В общем, вам следует избегать вызова Option.get, потому что это небезопасно, и обычно есть лучший способ добиться того, чего вы хотите (в этом случае использование соответствующего анализатора тела оказывается лучшим способом).

Теперь этот тест больше не компилируется, в отличие от исключения, вызванного None.get:

val fakeJson = """{ "foo":"bar" }"""
val fakeRequest = FakeRequest()
  .withHeaders("Content-type" -> "application/json")
  .withBody(fakeJson)
val index = controller.index()(fakeRequest)
status(index) must equalTo(OK)

Заставляя вас заменить его версией из вашего ответа:

val fakeJson = play.api.libs.json.Json.parse("""{ "foo":"bar" }""") 
val fakeRequest = FakeRequest().withBody(fakeJson)              
val index = controller.index()(fakeRequest)                         
status(index) must equalTo(OK)

Мое последнее предложение состоит в том, что вы используете Json.obj для очистки вашего теста:

val fakeJson = Json.obj("foo" -> "bar")
person danielnixon    schedule 15.02.2016
comment
Спасибо за очень подробный ответ. Я хотел бы спросить еще одну вещь: почему контроллер с parse.json НЕ анализирует тело fakeRequest (это строка), в то время как он автоматически анализирует тело POST запроса от клиента (что-то вроде curl)? В чем разница? - person iTakeshi; 15.02.2016
comment
ОБНОВЛЕНИЕ: изменено по вашему предложению, но тест не компилируется, говоря, что index является экземпляром play.api.libs.iteratee.Iteratee[Array[Byte], play.api.mvc.Result], но компилятор требует, чтобы он был scala.concurrent.Future[play.api.mvc.Result], поскольку Action.async ожидает этого. Я попытался изменить тестовый код на status(index.run), и он скомпилировался, но возникла ошибка времени выполнения (в ответе HTML было указано [Expecting text/json or application/json body] со статусом 415). Как я могу решить эту ситуацию? - person iTakeshi; 15.02.2016
comment
У меня работает val index = new Application().index()(FakeRequest().withBody(Json.obj("foo" -> "bar"))), за которым следует status(index) must equalTo(OK) (тест компилируется и проходит). - person danielnixon; 15.02.2016
comment
Э-э, только что сработало, заменив withJsonBody([JsValue]) на withBody([JsValue]), как в вашем комментарии... Меня это очень сбивает с толку. Ты хоть представляешь, чем отличается? - person iTakeshi; 15.02.2016
comment
Ах, извините за это. withBody дает вам FakeRequest[JsValue], а withJsonBody дает вам FakeRequest[AnyContentAsJson]. Вам нужен первый, потому что именно такой запрос вы получаете от Action.async(parse.json). - person danielnixon; 15.02.2016
comment
Читаю оригинальную реализацию playframework и наконец понял, что вы имели в виду! Большое спасибо! Усвоил много уроков из этого обсуждения. - person iTakeshi; 15.02.2016

[САМОСТОЯТЕЛЬНОЕ РЕШЕНИЕ]

Помучившись еще пару часов, проблема была решена с помощью withJsonBody:

"Application" should {                                                
  val controller = new Application                                    
  val fakeJson = play.api.libs.json.Json.parse("""{ "foo":"bar" }""") 
  val fakeRequest = FakeRequest().withJsonBody(fakeJson)              
  val index = controller.index()(fakeRequest)                         
  status(index) must equalTo(OK)                                      
}

Любые другие предложения приветствуются.

person iTakeshi    schedule 14.02.2016