Play Framework, Specs2 - вызов метода контроллера непосредственно из модульного теста

У меня есть метод в моем контроллере, который я хотел бы вызвать напрямую. Он принимает POST-форму, проверяет ее, а затем что-то возвращает. Я хочу проверить это напрямую, т.е. не проходить через помощник маршрутов.

Это мой код формы (FormFields - это просто класс case)

val searchForm = Form(
  mapping(
   "foo" -> nonEmptyText,
   "filter" -> optional(text).verifying("Filter text must be 'published' or 'unpublished'",
     x => x.isEmpty || x.get == "published" || x.get == "unpublished")
 )(FormFields.apply)(FormFields.unapply)

)

Это мой вызов контроллера.

def doThings() = IsAuthenticated {
   username => implicit request => {
    searchForm.bindFromRequest().fold(
      formWithErrors => BadRequest(s"Incorrect data: ${formWithErrors.errors.map(x => s"${x.key} ${x.message}").mkString}."),
      form => {
            OK("Test text here")
      }
    )
  }

}

Если я вызову это через файл маршрутов, как показано ниже, это будет работать так, как ожидалось. Форма публикуется, проверяется, возвращает OK ("Тест..."), как и ожидалось.

т.е. Нижеприведенные работы (с использованием Specs2)

        val request = FakeRequest(POST, "notarealurl")
          .withFormUrlEncodedBody(
          "filter" -> "published",
          "foo" -> "Test"
    ).withSession("email" -> "testuser")

    val Some(result) = route(request)
    status(result) must equalTo(OK)

Однако все, что я пытаюсь вызвать, метод терпит неудачу - сбой происходит на этапе проверки формы. Он говорит мне, что «foo» отсутствует значение, когда я запускаю модульный тест. Вот как я пытаюсь это сделать.

    val request = FakeRequest()
      .withFormUrlEncodedBody(
      "filter" -> "published",
      "foo" -> "Test"
    ).withSession("email" -> "testuser")


    //val Some(result) = route(request)
    val result = Search.searchProducts()(request)

    println(contentAsString(result))
    status(result) must equalTo(OK)

Напечатанный текст: «Неверный поиск: foo error.required». Я думаю, что неправильно звоню, но я не знаю, где я ошибаюсь.

Примечание. Код здесь представляет мою проблему, но он был сокращен, чтобы просто проиллюстрировать проблему.


person Ren    schedule 24.10.2013    source источник
comment
Я знаю, что вы заявили, что не хотите использовать помощник маршрутов. Но работает ли это, если вы его используете?   -  person Michael Zajac    schedule 24.10.2013
comment
@LimbSoup Да, это работает с помощником маршрутов.   -  person Ren    schedule 25.10.2013


Ответы (1)


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

В контроллере:

import play.api.data._
import play.api.data.Forms._
case class UserData(name: String, age: Int)
val userFormConstraints2 = Form(
  mapping(
    "name" -> nonEmptyText,
    "age" -> number(min = 0, max = 100)
  )(UserData.apply)(UserData.unapply)
)
def test = Action {
  implicit request => {
    userFormConstraints2.bindFromRequest().fold(
      formWithErrors => BadRequest("bad"),
      userData => {
        Ok(userData.name + userData.age)
      }
    )
  }
}

Тестовое задание:

class TempSpec extends Specification with MyHelpers {
  "1" can {
    "direct access to controller while posting" in new TestServer {
                        // `in new TestServer` spawns dependencies (`server`)
      val controller = new controllers.Kalva(server)
                        // I instantiate the controller passing the dependency
      val request = FakeRequest(POST, "bla")
        .withFormUrlEncodedBody(
          "name" -> "Richard",
          "age" -> "1"
        )
      val result = controller.test()(request)
      status(result) must equalTo(OK)
      contentAsString(result) must contain("Richard");
      val request_bad = FakeRequest(POST, "bla")
        .withFormUrlEncodedBody(
          "name" -> "",
          "age" -> "-1"
        )
      val result_bad = controller.test()(request_bad)
      status(result_bad) must equalTo(400)
      contentAsString(result_bad) must contain("bad");
    }
  }
}

Глобал.скала:

object Global extends GlobalSettings {
  private lazy val injector = Guice.createInjector(new TestModule)

  override def getControllerInstance[A](controller: Class[A]) = {
    injector.getInstance(controller)
  }
}

ТестМодуль:

import com.google.inject._
import com.tzavellas.sse.guice.ScalaModule
class TestModule extends ScalaModule {
  def configure() {
    @Provides
    def getServer:Server = {
      ...
    }
  }
}

В файле routes:

POST    /bla                        @controllers.Kalva.test
               // the `@` prefix is needed because of how we fetch controllers

Оригинальный ответ ниже:


class TranslateSpec extends Specification {

  "Translate" should {
    // The normal Play! way
    "accept a name, and return a proper greeting" in {
      running(FakeApplication()) {
        val translated = route(FakeRequest(GET, "/greet/Barney")).get

        status(translated) must equalTo(OK)
        contentType(translated) must beSome.which(_ == "text/html")
        contentAsString(translated) must contain ("Barney")  
      }
    }

      // Providing a fake Global, to explitly mock out the injector
    object FakeTranslatorGlobal extends play.api.GlobalSettings {
      override def getControllerInstance[A](clazz: Class[A]) = {
        new Translate(new FakeTranslator).asInstanceOf[A]
      }
    }
    "accept a name, and return a proper greeting (explicitly mocking module)" in {
      running(FakeApplication(withGlobal = Some(FakeTranslatorGlobal))) {
        val home = route(FakeRequest(GET, "/greet/Barney")).get
        contentAsString(home) must contain ("Hello Barney")
      }
    }

    // Calling the controller directly, without creating a new FakeApplication
    // (This is the fastest)
    "accept a name, and return a proper greeting (controller directly, no FakeApplication!)" in {
      val controller = new Translate(new FakeTranslator)
      val result = controller.greet(name = "Barney")(FakeRequest())
      contentAsString(result) must contain ("Hello Barney")
    }
  }
}

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

Этот конкретный отрывок взят из статьи «Почему я должен использовать DI с Play?» раздел. Статья посвящена настройке Google Guice с Play2 и возможностям, которые он открывает. Это практическое чтение.

Как вы можете видеть выше, «обычный способ Play!» — это хорошо, но, приняв DI, вы можете добиться гораздо большего в своих тестах (и, конечно, при разработке в целом).

Как описано в статье, использование Guice с Play требует внесения незначительных изменений в настройку Play по умолчанию, и оно того стоит. Я делаю это уже давно, и не оглядывался назад.

person Dominykas Mostauskis    schedule 25.10.2013
comment
Я не думаю, что это на самом деле решает мою проблему. Моя проблема не в том, чтобы получить доступ к контроллеру, я могу вызвать его напрямую (аналогично третьему подходу в вашем ответе), но сам вызов val result = Search.searchProducts()(request) проблематичен, когда я делаю СООБЩЕНИЕ. - person Ren; 28.10.2013
comment
Если вы запустите его в running(FakeApplication()) {, он сработает? Я предположил, что пропало. Использование DI освобождает вас от FakeApplication и всей этой путаницы с маршрутизацией. Как получить прямой доступ без него? - person Dominykas Mostauskis; 29.10.2013
comment
На самом деле это меньше DI и больше просто превращение контроллеров из объектов в классы. - person Dominykas Mostauskis; 29.10.2013
comment
Я пробовал с FakeApplication(), такое же поведение. Прямой доступ просто вызывал метод, к которому направляется контроллер (аналогично тому, как вы получили (val result = controller.greet(...)). Проблема возникает только при запросах POST, потому что тело, похоже, не Запросы GET работают нормально. Вам когда-нибудь удавалось получить проверенную форму POST (т.е. прочитать некоторые поля через помощник формы воспроизведения) при прямом вызове контроллера? - person Ren; 30.10.2013
comment
@Ren Я проверил это, и оно работает, я обновил ответ с помощью настроек, которые использовал. - person Dominykas Mostauskis; 30.10.2013