§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 // this file must be in a directory called "models"
import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._
@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
...
}
§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 extends Controller {
def index() = Action {
Ok("ok")
}
}
You can test it like:
import play.api.mvc._
import play.api.test._
import scala.concurrent.Future
class ExampleControllerSpec extends PlaySpecification 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 must be equalTo "ok"
}
}
}
§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
. For unit testing purposes, DefaultMessagesApi
can be instantiated without arguments, and will take a raw map.
You can test it like:
class ExampleFormSpec extends PlaySpecification with Results {
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)
"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!")))
}
}
}
§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"
}
}
}