§Writing functional tests with ScalaTest
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. The ScalaTest + Play integration library builds on this testing support for ScalaTest.
You can access all of Play’s built-in test support and ScalaTest + Play with the following imports:
import org.scalatest._
import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers.{GET => GET_REQUEST, _}
§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 = new GuiceApplicationBuilder()
.configure("some.configuration" -> "value")
.build()
If all or most tests in your test class need a Application
, and they can all share the same instance of Application
, mix in trait OneAppPerSuite
. You can access the Application
from the app
field. If you need to customize the Application
, override app
as shown in this example:
class ExampleSpec extends PlaySpec with OneAppPerSuite {
// Override app if you need a Application with other than
// default parameters.
implicit override lazy val app = new GuiceApplicationBuilder().configure(Map("ehcacheplugin" -> "disabled")).build()
"The OneAppPerSuite trait" must {
"provide an Application" in {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in {
Play.maybeApplication mustBe Some(app)
}
}
}
If you need each test to get its own Application
, instead of sharing the same one, use OneAppPerTest
instead:
class ExampleSpec extends PlaySpec with OneAppPerTest {
// Override newAppForTest if you need an Application with other than
// default parameters.
implicit override def newAppForTest(td: TestData) = new GuiceApplicationBuilder().configure(Map("ehcacheplugin" -> "disabled")).build()
"The OneAppPerTest trait" must {
"provide a new Application for each test" in {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in {
Play.maybeApplication mustBe Some(app)
}
}
}
The reason ScalaTest + Play provides both OneAppPerSuite
and OneAppPerTest
is to allow you to select the sharing strategy that makes your tests run fastest. If you want application state maintained between successive tests, you’ll need to use OneAppPerSuite
. If each test needs a clean slate, however, you could either use OneAppPerTest
or use OneAppPerSuite
, but clear any state at the end of each test. Furthermore, if your test suite will run fastest if multiple test classes share the same application, you can define a master suite that mixes in OneAppPerSuite
and nested suites that mix in ConfiguredApp
, as shown in the example in the documentation for ConfiguredApp
. You can use whichever strategy makes your test suite run the fastest.
§Testing with a server
Sometimes you want to test with the real HTTP stack. If all tests in your test class can reuse the same server instance, you can mix in OneServerPerSuite
(which will also provide a new Application
for the suite):
class ExampleSpec extends PlaySpec with OneServerPerSuite {
// Override app if you need an Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/") => Action { Ok("ok") }
}).build()
"test server logic" in {
val wsClient = app.injector.instanceOf[WSClient]
val myPublicAddress = s"localhost:$port"
val testPaymentGatewayURL = s"http://$myPublicAddress"
// The test payment gateway requires a callback to this server before it returns a result...
val callbackURL = s"http://$myPublicAddress/callback"
// await is from play.api.test.FutureAwaits
val response = await(wsClient.url(testPaymentGatewayURL).withQueryString("callbackURL" -> callbackURL).get())
response.status mustBe OK
}
}
If all tests in your test class require separate server instances, use OneServerPerTest
instead (which will also provide a new Application
for the suite):
class ExampleSpec extends PlaySpec with OneServerPerTest {
// Override newAppForTest if you need an Application with other than
// default parameters.
override def newAppForTest(testData: TestData) =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/") => Action { Ok("ok") }
}).build()
"The OneServerPerTest trait" must {
"test server logic" in {
val wsClient = app.injector.instanceOf[WSClient]
val myPublicAddress = s"localhost:$port"
val testPaymentGatewayURL = s"http://$myPublicAddress"
// The test payment gateway requires a callback to this server before it returns a result...
val callbackURL = s"http://$myPublicAddress/callback"
// await is from play.api.test.FutureAwaits
val response = await(wsClient.url(testPaymentGatewayURL).withQueryString("callbackURL" -> callbackURL).get())
response.status mustBe (OK)
}
}
}
The OneServerPerSuite
and OneServerPerTest
traits provide the port number on which the server is running as the port
field. By default this is 19001, however you can change this either overriding port
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.
You can also customize the Application
by overriding app
, as demonstrated in the previous examples.
Lastly, if allowing multiple test classes to share the same server will give you better performance than either the OneServerPerSuite
or OneServerPerTest
approaches, you can define a master suite that mixes in OneServerPerSuite
and nested suites that mix in ConfiguredServer
, as shown in the example in the documentation for ConfiguredServer
.
§Testing with a web browser
The ScalaTest + Play library builds on ScalaTest’s Selenium DSL to make it easy to test your Play applications from web browsers.
To run all tests in your test class using a same browser instance, mix OneBrowserPerSuite
into your test class. You’ll also need to mix in a BrowserFactory
trait that will provide a Selenium web driver: one of ChromeFactory
, FirefoxFactory
, HtmlUnitFactory
, InternetExplorerFactory
, SafariFactory
.
In addition to mixing in a BrowserFactory
, you will need to mix in a ServerProvider
trait that provides a TestServer
: one of OneServerPerSuite
, OneServerPerTest
, or ConfiguredServer
.
For example, the following test class mixes in OneServerPerSuite
and HtmUnitFactory
:
class ExampleSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory {
// Override app if you need a Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
"The OneBrowserPerTest trait" must {
"provide a web driver" in {
go to s"http://localhost:$port/testing"
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
}
If each of your tests requires a new browser instance, use OneBrowserPerTest
instead. As with OneBrowserPerSuite
, you’ll need to also mix in a ServerProvider
and BrowserFactory
:
class ExampleSpec extends PlaySpec with OneServerPerTest with OneBrowserPerTest with HtmlUnitFactory {
// Override newAppForTest if you need a Application with other than
// default parameters.
override def newAppForTest(testData: TestData) =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
"The OneBrowserPerTest trait" must {
"provide a web driver" in {
go to (s"http://localhost:$port/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
}
If you need multiple test classes to share the same browser instance, mix OneBrowserPerSuite
into a master suite and ConfiguredBrowser
into multiple nested suites. The nested suites will all share the same web browser. For an example, see the documentation for trait ConfiguredBrowser
.
§Running the same tests in multiple browsers
If you want to run tests in multiple web browsers, to ensure your application works correctly in all the browsers you support, you can use traits AllBrowsersPerSuite
or AllBrowsersPerTest
. Both of these traits declare a browsers
field of type IndexedSeq[BrowserInfo]
and an abstract sharedTests
method that takes a BrowserInfo
. The browsers
field indicates which browsers you want your tests to run in. The default is Chrome, Firefox, Internet Explorer, HtmlUnit
, and Safari. You can override browsers
if the default does not fit your needs. You place tests you want to run in multiple browsers in the sharedTests
method, placing the name of the browser at the end of each test name. (The browser name is available from the BrowserInfo
passed into sharedTests
.) Here is an example that uses AllBrowsersPerSuite
:
class ExampleSpec extends PlaySpec with OneServerPerSuite with AllBrowsersPerSuite {
// Override app if you need an Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build
def sharedTests(browser: BrowserInfo) = {
"The AllBrowsersPerSuite trait" must {
"provide a web driver " + browser.name in {
go to s"http://localhost:$port/testing"
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
}
}
All tests declared by sharedTests
will be run with all browsers mentioned in the browsers
field, so long as they are available on the host system. Tests for any browser that is not available on the host system will be canceled automatically.
Note: You need to append the
browser.name
manually to the test name to ensure each test in the suite has a unique name (which is required by ScalaTest). If you leave that off, you’ll get a duplicate-test-name error when you run your tests.
AllBrowsersPerSuite
will create a single instance of each type of browser and use that for all the tests declared in sharedTests
. If you want each test to have its own, brand new browser instance, use AllBrowsersPerTest
instead:
class ExampleSpec extends PlaySpec with OneServerPerSuite with AllBrowsersPerTest {
// Override app if you need an Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
def sharedTests(browser: BrowserInfo) = {
"The AllBrowsersPerTest trait" must {
"provide a web driver" + browser.name in {
go to (s"http://localhost:$port/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
}
}
Although both AllBrowsersPerSuite
and AllBrowsersPerTest
will cancel tests for unavailable browser types, the tests will show up as canceled in the output. To can clean up the output, you can exclude web browsers that will never be available by overriding browsers
, as shown in this example:
class ExampleOverrideBrowsersSpec extends PlaySpec with OneServerPerSuite with AllBrowsersPerSuite {
override lazy val browsers =
Vector(
FirefoxInfo(firefoxProfile),
ChromeInfo
)
// Override app if you need an Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
def sharedTests(browser: BrowserInfo) = {
"The AllBrowsersPerSuite trait" must {
"provide a web driver" + browser.name in {
go to (s"http://localhost:$port/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
}
}
The previous test class will only attempt to run the shared tests with Firefox and Chrome (and cancel tests automatically if a browser is not available).
§PlaySpec
PlaySpec
provides a convenience “super Suite” ScalaTest base class for Play tests. You get WordSpec
, MustMatchers
, OptionValues
, and WsScalaTestClient
automatically by extending PlaySpec
:
class ExampleSpec extends PlaySpec with OneServerPerSuite with ScalaFutures with IntegrationPatience {
// Override app if you need an Application with other than
// default parameters.
implicit override lazy val app =
new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
"WsScalaTestClient's" must {
"wsUrl works correctly" in {
val futureResult = wsUrl("/testing").get
val body = futureResult.futureValue.body
val expectedBody =
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
assert(body == expectedBody)
}
"wsCall works correctly" in {
val futureResult = wsCall(Call("get", "/testing")).get
val body = futureResult.futureValue.body
val expectedBody =
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
assert(body == expectedBody)
}
}
}
You can mix any of the previously mentioned traits into PlaySpec
.
§When different tests need different fixtures
In all the test classes shown in previous examples, all or most tests in the test class required the same fixtures. While this is common, it is not always the case. If different tests in the same test class need different fixtures, mix in trait MixedFixtures
. Then give each individual test the fixture it needs using one of these no-arg functions: App, Server, Chrome, Firefox, HtmlUnit, InternetExplorer, or Safari.
You cannot mix MixedFixtures
into PlaySpec
because MixedFixtures
requires a ScalaTest fixture.Suite
and PlaySpec
is just a regular Suite
. If you want a convenient base class for mixed fixtures, extend MixedPlaySpec
instead. Here’s an example:
// MixedPlaySpec already mixes in MixedFixtures
class ExampleSpec extends MixedPlaySpec {
// Some helper methods
def buildApp[A](elems: (String, String)*) =
new GuiceApplicationBuilder().configure(Map(elems:_*)).router(Router.from {
case GET(p"/testing") =>
Action(
Results.Ok(
"<html>" +
"<head><title>Test Page</title></head>" +
"<body>" +
"<input type='button' name='b' value='Click Me' onclick='document.title=\"scalatest\"' />" +
"</body>" +
"</html>"
).as("text/html")
)
}).build()
def getConfig(key: String)(implicit app: Application) = app.configuration.getString(key)
// If a test just needs an Application, use "new App":
"The App function" must {
"provide an Application" in new App(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new App(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new App(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
}
// If a test needs an Application and running TestServer, use "new Server":
"The Server function" must {
"provide an Application" in new Server(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new Server(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new Server(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new Server {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
}
// If a test needs an Application, running TestServer, and Selenium
// HtmlUnit driver use "new HtmlUnit":
"The HtmlUnit function" must {
"provide an Application" in new HtmlUnit(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new HtmlUnit(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new HtmlUnit(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new HtmlUnit {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
"provide a web driver" in new HtmlUnit(buildApp()) {
go to ("http://localhost:" + port + "/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
// If a test needs an Application, running TestServer, and Selenium
// Firefox driver use "new Firefox":
"The Firefox function" must {
"provide an application" in new Firefox(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new Firefox(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new Firefox(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new Firefox {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
"provide a web driver" in new Firefox(buildApp()) {
go to ("http://localhost:" + port + "/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
// If a test needs an Application, running TestServer, and Selenium
// Safari driver use "new Safari":
"The Safari function" must {
"provide an Application" in new Safari(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new Safari(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new Safari(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new Safari {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
"provide a web driver" in new Safari(buildApp()) {
go to ("http://localhost:" + port + "/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
// If a test needs an Application, running TestServer, and Selenium
// Chrome driver use "new Chrome":
"The Chrome function" must {
"provide an Application" in new Chrome(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new Chrome(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new Chrome(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new Chrome {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
"provide a web driver" in new Chrome(buildApp()) {
go to ("http://localhost:" + port + "/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
// If a test needs an Application, running TestServer, and Selenium
// InternetExplorer driver use "new InternetExplorer":
"The InternetExplorer function" must {
"provide an Application" in new InternetExplorer(buildApp("ehcacheplugin" -> "disabled")) {
app.configuration.getString("ehcacheplugin") mustBe Some("disabled")
}
"make the Application available implicitly" in new InternetExplorer(buildApp("ehcacheplugin" -> "disabled")) {
getConfig("ehcacheplugin") mustBe Some("disabled")
}
"start the Application" in new InternetExplorer(buildApp("ehcacheplugin" -> "disabled")) {
Play.maybeApplication mustBe Some(app)
}
import Helpers._
"send 404 on a bad request" in new InternetExplorer {
import java.net._
val url = new URL("http://localhost:" + port + "/boom")
val con = url.openConnection().asInstanceOf[HttpURLConnection]
try con.getResponseCode mustBe 404
finally con.disconnect()
}
"provide a web driver" in new InternetExplorer(buildApp()) {
go to ("http://localhost:" + port + "/testing")
pageTitle mustBe "Test Page"
click on find(name("b")).value
eventually { pageTitle mustBe "scalatest" }
}
}
// If a test does not need any special fixtures, just
// write "in { () => ..."
"Any old thing" must {
"be doable without much boilerplate" in { () =>
1 + 1 mustEqual 2
}
}
}
§Testing a 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 App {
val html = views.html.index("Coco")
contentAsString(html) must include ("Hello Coco")
}
§Testing a controller
You can call any Action
code by providing a FakeRequest
:
import scala.concurrent.Future
import org.scalatestplus.play._
import play.api.mvc._
import play.api.test._
import play.api.test.Helpers._
class ExampleControllerSpec extends PlaySpec with Results {
"Example Page#index" should {
"should be valid" in {
val controller = new ExampleController()
val result: Future[Result] = controller.index().apply(FakeRequest())
val bodyText: String = contentAsString(result)
bodyText mustBe "ok"
}
}
}
§Testing the router
Instead of calling the Action
yourself, you can let the Router
do it:
"respond to the index Action" in new App(applicationWithRouter) {
val Some(result) = route(app, FakeRequest(GET_REQUEST, "/Bob"))
status(result) mustEqual OK
contentType(result) mustEqual Some("text/html")
charset(result) mustEqual Some("utf-8")
contentAsString(result) must include ("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
.
val appWithMemoryDatabase = new GuiceApplicationBuilder().configure(inMemoryDatabase("test")).build()
"run an application" in new App(appWithMemoryDatabase) {
val Some(macintosh) = Computer.findById(21)
macintosh.name mustEqual "Macintosh"
macintosh.introduced.value mustEqual "1984-01-24"
}
§Testing WS calls
If you are calling a web service, you can use WSTestClient
. There are two calls available, wsCall
and wsUrl
that will take a Call or a string, respectively. Note that they expect to be called in the context of WithApplication
.
wsCall(controllers.routes.Application.index()).get()
wsUrl("http://localhost:9000").get()
Next: Testing with specs2