Documentation

You are viewing the documentation for the 2.7.0-M4 development release. The latest stable release series is 3.0.x.

§Writing functional tests with specs2

Play provides a number of classes and convenience methods that assist with functional testing. Most of these can be found either in the play.api.test package or in the Helpers object.

You can add these methods and classes by importing the following:

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

§Creating Application instances for testing

Play frequently requires a running Application as context. If you’re using the default Guice dependency injection, you can use the GuiceApplicationBuilder class which can be configured with different configuration, routes, or even additional modules.

val application: Application = GuiceApplicationBuilder().build()

§WithApplication

To pass in an application to an example, use WithApplication. An explicit Application can be passed in, but a default application (created from the default GuiceApplicationBuilder) is provided for convenience.

Because WithApplication is a built in Around block, you can override it to provide your own data population:

abstract class WithDbData extends WithApplication {
  override def around[T: AsResult](t: => T): Result = super.around {
    setupData()
    t
  }

  def setupData(): Unit = {
    // setup data
  }
}

"Computer model" should {

  "be retrieved by id" in new WithDbData {
    // your test code
  }
  "be retrieved by email" in new WithDbData {
    // your test code
  }
}

§WithServer

Sometimes you want to test the real HTTP stack from within your test, in which case you can start a test server using WithServer:

"test server logic" in new WithServer(app = applicationWithBrowser, port = testPort) {
  // The test payment gateway requires a callback to this server before it returns a result...
  val callbackURL = s"http://$myPublicAddress/callback"

  val ws = app.injector.instanceOf[WSClient]

  // await is from play.api.test.FutureAwaits
  val response = await(ws.url(testPaymentGatewayURL).withQueryStringParameters("callbackURL" -> callbackURL).get())

  response.status must equalTo(OK)
}

The port value contains the port number the server is running on. By default this is 19001, however you can change this either by passing the port into WithServer, or by setting the system property testserver.port. This can be useful for integrating with continuous integration servers, so that ports can be dynamically reserved for each build.

An application can also be passed to the test server, which is useful for setting up custom routes and testing WS calls:

val appWithRoutes = GuiceApplicationBuilder().appRoutes {app =>
  val Action = app.injector.instanceOf[DefaultActionBuilder]
  ({
    case ("GET", "/") => Action {
      Ok("ok")
    }
  })
}.build()

"test WSClient logic" in new WithServer(app = appWithRoutes, port = 3333) {
  val ws = app.injector.instanceOf[WSClient]
  await(ws.url("http://localhost:3333").get()).status must equalTo(OK)
}

§WithBrowser

If you want to test your application using a browser, you can use Selenium WebDriver. Play will start the WebDriver for you, and wrap it in the convenient API provided by FluentLenium using WithBrowser. Like WithServer, you can change the port, Application, and you can also select the web browser to use:

def applicationWithBrowser = {
  new GuiceApplicationBuilder().appRoutes { app =>
    val Action = app.injector.instanceOf[DefaultActionBuilder]
    ({
      case ("GET", "/") =>
        Action {
          Ok(
            """
              |<html>
              |<body>
              |  <div id="title">Hello Guest</div>
              |  <a href="/login">click me</a>
              |</body>
              |</html>
            """.stripMargin) as "text/html"
        }
      case ("GET", "/login") =>
        Action {
          Ok(
            """
              |<html>
              |<body>
              |  <div id="title">Hello Coco</div>
              |</body>
              |</html>
            """.stripMargin) as "text/html"
        }
    })
  }.build()
}

"run in a browser" in new WithBrowser(webDriver = WebDriverFactory(HTMLUNIT), app = applicationWithBrowser) {
  browser.goTo("/")

  // Check the page
  browser.el("#title").text() must equalTo("Hello Guest")

  browser.el("a").click()

  browser.url must equalTo("login")
  browser.el("#title").text() must equalTo("Hello Coco")
}

§Injecting

There are many functional tests that use the injector directly through the implicit app:

"test" in new WithApplication() {
  val executionContext = app.injector.instanceOf[ExecutionContext]
  executionContext must beAnInstanceOf[ExecutionContext]
}

With the Injecting trait, you can elide this:

"test" in new WithApplication() with play.api.test.Injecting {
  val executionContext = inject[ExecutionContext]
  executionContext must beAnInstanceOf[ExecutionContext]
}

§PlaySpecification

PlaySpecification is an extension of Specification that excludes some of the mixins provided in the default specs2 specification that clash with Play helpers methods. It also mixes in the Play test helpers and types for convenience.

class ExamplePlaySpecificationSpec extends PlaySpecification {
  "The specification" should {

    "have access to HeaderNames" in {
      USER_AGENT must be_===("User-Agent")
    }

    "have access to Status" in {
      OK must be_===(200)
    }
  }
}

§Testing a view template

Since a template is a standard Scala function, you can execute it from your test, and check the result:

"render index template" in new WithApplication {
  val html = views.html.index("Coco")

  contentAsString(html) must contain("Hello Coco")
}

§Testing a controller

You can call any Action code by providing a FakeRequest:

"respond to the index Action" in new WithApplication {
  val controller = app.injector.instanceOf[scalaguide.tests.controllers.HomeController]
  val result = controller.index()(FakeRequest())

  status(result) must equalTo(OK)
  contentType(result) must beSome("text/plain")
  contentAsString(result) must contain("Hello Bob")
}

Technically, you don’t need WithApplication here, because you can instantiate the controller directly – however, direct controller instantiation is more of a unit test of the controller than a functional test.

§Testing the router

Instead of calling the Action yourself, you can let the Router do it:

"respond to the index Action" in new WithApplication(applicationWithRouter) {
val Some(result) = route(app, FakeRequest(GET, "/Bob"))

  status(result) must equalTo(OK)
  contentType(result) must beSome("text/html")
  charset(result) must beSome("utf-8")
  contentAsString(result) must contain("Hello Bob")
}

§Testing a model

If you are using an SQL database, you can replace the database connection with an in-memory instance of an H2 database using inMemoryDatabase.

def appWithMemoryDatabase = new GuiceApplicationBuilder().configure(inMemoryDatabase("test")).build()
"run an application" in new WithApplication(appWithMemoryDatabase) {

  val Some(macintosh) = Computer.findById(21)

  macintosh.name must equalTo("Macintosh")
  macintosh.introduced must beSome.which(_ must beEqualTo("1984-01-24"))
}

§Testing Messages API

For functional tests that involve configuration, the best option is to use WithApplication and pull in an injected MessagesApi:

"messages" should {
  import play.api.i18n._

  implicit val lang = Lang("en-US")

  "provide default messages with the Java API" in new WithApplication() with Injecting {
    val javaMessagesApi = inject[play.i18n.MessagesApi]
    val msg = javaMessagesApi.get(new play.i18n.Lang(lang), "constraint.email")
    msg must ===("Email")
  }

  "provide default messages with the Scala API" in new WithApplication() with Injecting {
    val messagesApi = inject[MessagesApi]
    val msg = messagesApi("constraint.email")
    msg must ===("Email")
  }
}

If you need to customize the configuration, it’s better to add configuration values into the GuiceApplicationBuilder rather than use the DefaultMessagesApiProvider directly.

Next: Testing with Guice