Documentation

You are viewing the documentation for the 2.6.21 release in the 2.6.x series of releases. The latest stable release series is 3.0.x.

§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.

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:

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"
    }
  }

}

§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 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 MessagesApi:

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.

Next: Writing functional tests with specs2