§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).withQueryString("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().router(Router.from {
case GET(p"/") => Action {
Ok("ok")
}
}).build()
"test WS 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().router(Router.from {
case GET(p"/") =>
Action {
Ok(
"""
|<html>
|<body>
| <div id="title">Hello Guest</div>
| <a href="/login">click me</a>
|</body>
|</html>
""".stripMargin) as "text/html"
}
case GET(p"/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.$("#title").text() must equalTo("Hello Guest")
browser.$("a").click()
browser.url must equalTo("login")
browser.$("#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