§Testing your application with specs2
Writing tests for your application can be an involved process. Play provides a default test framework for you, and provides helpers and application stubs to make testing your application as easy as possible.
§Overview
The location for tests is in the “test” folder. There are two sample test files created in the test folder which can be used as templates.
You can run tests from the Play console.
- To run all tests, run
test
. - To run only one test class, run
test-only
followed by the name of the class i.e.test-only my.namespace.MySpec
. - To run only the tests that have failed, run
test-quick
. - To run tests continually, run a command with a tilde in front, i.e.
~test-quick
. - To access test helpers such as
FakeRequest
in console, runtest:console
.
Testing in Play is based on SBT, and a full description is available in the testing SBT chapter.
§Using specs2
To use Play’s specs2 support, add the Play specs2 dependency to your build as a test scoped dependency:
libraryDependencies += specs2 % Test
In specs2, tests are organized into specifications, which contain examples which run the system under test through various different code paths.
Specifications extend the Specification
trait and are using the should/in format:
import org.specs2.mutable._
class HelloWorldSpec extends Specification {
"The 'Hello world' string" should {
"contain 11 characters" in {
"Hello world" must have size(11)
}
"start with 'Hello'" in {
"Hello world" must startWith("Hello")
}
"end with 'world'" in {
"Hello world" must endWith("world")
}
}
}
Specifications can be run in either IntelliJ IDEA (using the Scala plugin) or in Eclipse (using the Scala IDE). Please see the IDE page for more details.
Note: Due to a bug in the presentation compiler, tests must be defined in a specific format to work with Eclipse:
- The package must be exactly the same as the directory path.
- The specification must be annotated with
@RunWith(classOf[JUnitRunner])
.
Here is a valid specification for Eclipse:
package models
import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner
@RunWith(classOf[JUnitRunner])
class UserSpec extends Specification {
"User" should {
"have a name" in {
val user = User(id = "user-id", name = "Player", email = "[email protected]")
user.name must beEqualTo("Player")
}
}
}
§Matchers
When you use an example, you must return an example result. Usually, you will see a statement containing a must
:
"Hello world" must endWith("world")
The expression that follows the must
keyword are known as matchers
. Matchers return an example result, typically Success or Failure. The example will not compile if it does not return a result.
The most useful matchers are the match results. These are used to check for equality, determine the result of Option and Either, and even check if exceptions are thrown.
There are also optional matchers that allow for XML and JSON matching in tests.
§Mockito
Mocks are used to isolate unit tests against external dependencies. For example, if your class depends on an external DataService
class, you can feed appropriate data to your class without instantiating a DataService
object.
Mockito is integrated into specs2 as the default mocking library.
To use Mockito, add the following import:
import org.specs2.mock._
You can mock out references to classes like so:
trait DataService {
def findData: Data
}
case class Data(retrievalDate: java.util.Date)
import org.specs2.mock._
import org.specs2.mutable._
import java.util._
class ExampleMockitoSpec extends Specification with Mockito {
"MyService#isDailyData" should {
"return true if the data is from today" in {
val mockDataService = mock[DataService]
mockDataService.findData returns Data(retrievalDate = new java.util.Date())
val myService = new MyService() {
override def dataService = mockDataService
}
val actual = myService.isDailyData
actual must equalTo(true)
}
}
}
Mocking is especially useful for testing the public methods of classes. Mocking objects and private methods is possible, but considerably harder.
§Unit Testing Models
Play does not require models to use a particular database data access layer. However, if the application uses Anorm or Slick, then frequently the Model will have a reference to database access internally.
import anorm._
import anorm.SqlParser._
case class User(id: String, name: String, email: String) {
def roles = DB.withConnection { implicit connection =>
...
}
}
For unit testing, this approach can make mocking out the roles
method tricky.
A common approach is to keep the models isolated from the database and as much logic as possible, and abstract database access behind a repository layer.
case class Role(name: String)
case class User(id: String, name: String, email: String)
trait UserRepository {
def roles(user: User): Set[Role]
}
class AnormUserRepository extends UserRepository {
import anorm._
import anorm.SqlParser._
def roles(user:User) : Set[Role] = {
...
}
}
and then access them through services:
class UserService(userRepository: UserRepository) {
def isAdmin(user: User): Boolean = {
userRepository.roles(user).contains(Role("ADMIN"))
}
}
In this way, the isAdmin
method can be tested by mocking out the UserRepository
reference and passing it into the service:
class UserServiceSpec extends Specification with Mockito {
"UserService#isAdmin" should {
"be true when the role is admin" in {
val userRepository = mock[UserRepository]
userRepository.roles(any[User]) returns Set(Role("ADMIN"))
val userService = new UserService(userRepository)
val actual = userService.isAdmin(User("11", "Steve", "[email protected]"))
actual must beTrue
}
}
}
§Unit Testing Controllers
Since your controllers are just regular classes, you can easily unit test them using Play helpers. If your controllers depends on another classes, using dependency injection will enable you to mock these dependencies. Per instance, given the following controller:
class ExampleController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {
def index() = Action {
Ok("ok")
}
}
You can test it like:
import javax.inject.Inject
import play.api.i18n.Messages
import play.api.mvc._
import play.api.test._
import scala.concurrent.Future
class ExampleControllerSpec extends PlaySpecification with Results {
"Example Page#index" should {
"be valid" in {
val controller = new ExampleController(Helpers.stubControllerComponents())
val result: Future[Result] = controller.index().apply(FakeRequest())
val bodyText: String = contentAsString(result)
bodyText must be equalTo "ok"
}
}
}
// #scalatest-exampleformspec
object FormData {
import play.api.data.Forms._
import play.api.data._
import play.api.i18n._
import play.api.libs.json._
val form = Form(
mapping(
"name" -> text,
"age" -> number(min = 0)
)(UserData.apply)(UserData.unapply)
)
case class UserData(name: String, age: Int)
}
class ExampleFormSpec extends PlaySpecification with Results {
import play.api.data._
import play.api.i18n._
import play.api.libs.json._
import FormData._
"Form" should {
"be valid" in {
val messagesApi = new DefaultMessagesApi(
Map("en" ->
Map("error.min" -> "minimum!")
)
)
implicit val request = {
FakeRequest("POST", "/")
.withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
}
implicit val messages = messagesApi.preferred(request)
def errorFunc(badForm: Form[UserData]) = {
BadRequest(badForm.errorsAsJson)
}
def successFunc(userData: UserData) = {
Redirect("/").flashing("success" -> "success form!")
}
val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("minimum!")))
}
}
}
// #scalatest-exampleformspec
§StubControllerComponents
The StubControllerComponentsFactory
creates a stub ControllerComponents
that can be used for unit testing a controller:
val controller = new MyController(
Helpers.stubControllerComponents(bodyParser = stubParser)
)
§StubBodyParser
The StubBodyParserFactory
creates a stub BodyParser
that can be used for unit testing content:
val stubParser = Helpers.stubBodyParser(AnyContent("hello"))
§Unit Testing Forms
Forms are also just regular classes, and can unit tested using Play’s Test Helpers. Using play.api.test.FakeRequest
, you can call form.bindFromRequest
and test for errors against any custom constraints.
To unit test form processing and render validation errors, you will want a MessagesApi
instance in implicit scope. The default implementation of MessagesApi
is DefaultMessagesApi
:
You can test it like:
object FormData {
import play.api.data.Forms._
import play.api.data._
import play.api.i18n._
import play.api.libs.json._
val form = Form(
mapping(
"name" -> text,
"age" -> number(min = 0)
)(UserData.apply)(UserData.unapply)
)
case class UserData(name: String, age: Int)
}
class ExampleFormSpec extends PlaySpecification with Results {
import play.api.data._
import play.api.i18n._
import play.api.libs.json._
import FormData._
"Form" should {
"be valid" in {
val messagesApi = new DefaultMessagesApi(
Map("en" ->
Map("error.min" -> "minimum!")
)
)
implicit val request = {
FakeRequest("POST", "/")
.withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
}
implicit val messages = messagesApi.preferred(request)
def errorFunc(badForm: Form[UserData]) = {
BadRequest(badForm.errorsAsJson)
}
def successFunc(userData: UserData) = {
Redirect("/").flashing("success" -> "success form!")
}
val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("minimum!")))
}
}
}
When rendering a template that takes form helpers, you can pass in a Messages the same way, or use Helpers.stubMessages
:
class ExampleTemplateSpec extends PlaySpecification {
import play.api.data._
import FormData._
"Example Template with Form" should {
"be valid" in {
val form: Form[UserData] = FormData.form
implicit val messages: Messages = Helpers.stubMessages()
contentAsString(views.html.formTemplate(form)) must contain("ok")
}
}
}
Or, if you are using a form that uses CSRF.formField
and requires an implicit request, you can use [MessagesRequest
] in the template and use Helpers.stubMessagesRequest
:
class ExampleTemplateWithCSRFSpec extends PlaySpecification {
import play.api.data._
import FormData._
"Example Template with Form" should {
"be valid" in {
val form: Form[UserData] = FormData.form
implicit val messageRequestHeader: MessagesRequestHeader = Helpers.stubMessagesRequest()
contentAsString(views.html.formTemplateWithCSRF(form)) must contain("ok")
}
}
}
§Unit Testing EssentialAction
Testing Action
or Filter
can require to test an EssentialAction
(more information about what an EssentialAction is)
For this, the test Helpers.call
can be used like that:
class ExampleEssentialActionSpec extends PlaySpecification {
"An essential action" should {
"can parse a JSON body" in new WithApplication() {
val action: EssentialAction = Action { request =>
val value = (request.body.asJson.get \ "field").as[String]
Ok(value)
}
val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))
val result = call(action, request)
status(result) mustEqual OK
contentAsString(result) mustEqual "value"
}
}
}
§Unit Testing Messages
For unit testing purposes, DefaultMessagesApi
can be instantiated without arguments, and will take a raw map, so you can test forms and validation failures against custom MessageApi:
class ExampleMessagesSpec extends PlaySpecification with ControllerHelpers {
import play.api.libs.json.Json
import play.api.data.Forms._
import play.api.data.Form
import play.api.i18n._
case class UserData(name: String, age:Int)
"Messages test" should {
"test messages validation in forms" in {
// Define a custom message against the number validation constraint
val messagesApi = new DefaultMessagesApi(
Map("en" -> Map("error.min" -> "CUSTOM MESSAGE"))
)
// Called when form validation fails
def errorFunc(badForm: Form[UserData])(implicit request: RequestHeader) = {
implicit val messages = messagesApi.preferred(request)
BadRequest(badForm.errorsAsJson)
}
// Called when form validation succeeds
def successFunc(userData: UserData) = Redirect("/")
// Define an age with 0 as the minimum
val form = Form(
mapping("name" -> text, "age" -> number(min = 0))
(UserData.apply)(UserData.unapply)
)
// Submit a request with age = -1
implicit val request = {
play.api.test.FakeRequest("POST", "/")
.withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
}
// Verify that the "error.min" is the custom message
val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("CUSTOM MESSAGE")))
}
}
}
You can also use Helpers.stubMessagesApi()
in testing to provide a premade empty MessagesApi.