Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play Framework, Specs2 - Calling controller method directly from unit test

I have a method in my controller that I'd like to call directly. It takes in a POSTed form, validates it, then returns something. I want to test this directly - i.e. not go through the routes helper.

This is my Form code (FormFields is just a case class)

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)

)

This is my controller call.

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")
      }
    )
  }

}

If I call this via my routes file, as below - This works as expected. Form gets posted, verified, returns OK("Test...") as expected.

ie. The below works (using Specs2)

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

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

However, whatever I try to call the method directly fails - The failure happens on the form validation step. It tells me that "foo" is missing a value when I run the unit test. This is how I'm attempting to do this.

    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)

The text printed is "Incorrect search: foo error.required." I think I'm not doing the call properly, but I don't know where I'm going wrong.

Note : The code here represents my problem but has been cut down to just illustrate the issue.

like image 538
Ren Avatar asked Oct 24 '13 04:10

Ren


1 Answers

I mimicked your logic, and it runs fine. I replaced some of your code with copy-paste from Play documentation just to keep it minimal. I tested it on top of a setup I'm working on now so you'll see artifacts foreign to the default Play setup. This setup is more or less identical to the one described in the article I linked originally. I wouldn't know how to get more direct than this:

In controller:

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)
      }
    )
  }
}

Test:

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");
    }
  }
}

Global.scala:

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

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

TestModule:

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

Within routes file:

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

Original answer below:


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")
    }
  }
}

The code above is quite self-descriptive and it shows the default testing workflow and how it can be improved with Dependency Injection. It's a quote from this article.

This particular excerpt is from the "Why should I use DI with Play?" section. The article is about setting up Google Guice with Play2 and the possibilities it opens up. It's a practical read.

As you can see above, "The normal Play! way" is fine, but by embracing DI you can get away with so much more in your tests (and development in general of course).

As is described in the article, using Guice with Play involves making minor changes to Play's default set-up and it's well worth it. I've been doing it this way for a long time now, and haven't looked back.

like image 152
Dominykas Mostauskis Avatar answered Sep 23 '22 11:09

Dominykas Mostauskis