Testing RESTful PlayFramework applications.
35 minutes reading
Code bible, there are several aspects that we have to pay attention when we design our tests:
– Minimize the number of assert per concept.
– Test just one concept per test function.
– Test should not depend on each other.
– Test should run in any environment.
Test should pass or fail BUT you should not make process or any other thing for check if the Test was OK or failed.
Write the TEST first OR with the code, NOT after it be running in a production environment.
I am going to talk about Test Implementation in Scala and more specific in Scala Playframework applications. If you come from Java programming language perhaps the more easy way is by ScalaTest – FlatSpec, it is a good starting point.
So when we are trying UNIT TEST in Play Framework, our minimal tests should include the followings artifacts:
For make the previous topics more easy I have some IMPORTANT tips:
The main ideas are:
In an “Ideal Hello World Play Framework application” EVERY THINGS is very simple BUT the problems arise in Real World application with escenarios like that:
I will talk here about testing problems that are not clearly treated in Play documentation and how tackle it. When you are in real life with play framework applications is when you understand how tedious is mock a binding in Guice when you trying to create Fake apps for Testing.
It is more easy MockitoSugar in this context and could be better if when you mock any component in Guice we could have the MockitoSugar concept instead of having to create a Well Mocked Object and binding it when you try to make an App via Guice Builder.
A design made for an easy/smart test:
I will not tell here why is so important dependency injection, you can find that information in every where even in playframework documentation.
In the following example I will show a controller that manage the actions:
class FootballLeagueController @Inject()(action: DefaultControllerComponents, services: TDataServices) extends BaseController with ContentNegotiation { /** * Best way, using a wrapper for an action that process a POST with a JSON * in the request. * * @see utilities.DefaultControllerComponents.jsonActionBuilder a reference * to JsonActionBuilder a builder of actions that process POST request * @return specific Result when every things is Ok so we send the status * and the comment with the specific json that show the result */ def insertMatchWithCustomized = action.jsonActionBuilder{ implicit request => val matchGame: Match = request.body.asJson.get.as[Match] processContentNegotiationForJson[Match](matchGame) } // .............. def insertMatchGeneric = JsonAction[Match](matchReads){ implicit request => val matchGame: Match = request.body.asJson.get.as[Match] processContentNegotiationForJson[Match](matchGame) } // .......... def getMatchGame = action.defaultActionBuilder { implicit request => val dataResults:Seq[Match] = services.modelOfMatchFootball("football.txt") proccessContentNegotiation[Match](dataResults) } .............. } |
code_example 1 ref. source code Controller
Some of my recommendations for the real life Controllers:
In our Controller [ref. code_example 1] we are going to Test first the Controller:
/** * Created by ldipotet on 23/09/17 */ package com.ldg.play.test import com.ldg.basecontrollers.{DefaultActionBuilder, DefaultControllerComponents, JsonActionBuilder} ....... import com.ldg.model.Match import com.ldg.play.baseclass.UnitSpec import controllers.FootballLeagueController import org.mockito.Mockito._ import org.scalatest.mock.MockitoSugar import play.api.test.FakeRequest import play.api.test.Helpers._ ....... import services.TDataServices class FootballControllerSpec extends UnitSpec with MockitoSugar { val mockDataServices = mock[TDataServices] val mockDefaultControllerComponents = mock[DefaultControllerComponents] val mockActionBuilder = new JsonActionBuilder() val mockDefaultActionBuilder = new DefaultActionBuilder() ........ val TestDefaultControllerComponents: DefaultControllerComponents = DefaultControllerComponents(mockDefaultActionBuilder,mockActionBuilder,/*messagesApi,*/langs) val TestFootballleagueController = new FootballLeagueController(TestDefaultControllerComponents,mockDataServices) /** * * Testing right response for acceptance of application/header * Request: plain text * Response: a Json * */ "Request /GET/ with Content-Type:text/plain and application/json" should "return a json file Response with a 200 Code" in { when(mockDataServices.modelOfMatchFootball("football.txt")) thenReturn Seq(matchGame) val request = FakeRequest(GET, "/football/matchs") .withHeaders(("Accept","application/json"),("Content-Type","text/plain")) val result = TestFootballleagueController.getMatchGame(request) val resultMatchGame: Match = (contentAsJson(result) \ "message" \ 0).as[Match] status(result) shouldBe OK resultMatchGame.homeTeam.name should equal(matchGame.homeTeam.name) resultMatchGame.homeTeam.goals should equal(matchGame.homeTeam.goals) } /** * * Testing right response for acceptance of application/header * Testing template work fine: Result of a mocked template shoulBe equal to Res * Request: plain text * Response: a CSV file * */ "Request /GET/ with Content-Type:text/plain and txt/csv" should "return a csv file Response with a 200 Code" in { when(mockDataServices.modelOfMatchFootball("football.txt")) thenReturn Seq(matchGame) val request = FakeRequest(GET, "/football/matchs").withHeaders(("Accept","text/csv"),("Content-Type","text/plain")) val result = TestFootballleagueController.getMatchGame(request) val content = views.csv.football(Seq(matchGame)) val templateContent: String = contentAsString(content) val resultContent: String = contentAsString(result) status(result) shouldBe OK templateContent should equal(resultContent) } "Request /GET with Content-Type:text/plain and WRONG Accept: image/jpeg" should "return a json file Response with a 406 Code" in { val result = TestFootballleagueController.getMatchGame .apply(FakeRequest(GET, "/").withHeaders(("Accept","image/jpeg"),("Content-Type","text/plain"))) status(result) shouldBe NOT_ACCEPTABLE } } |
code_example 2 ref. source code ControllerSpec
So if I am planning to test my Controller and if at the same time it is being injected by several component, the first thing that I need to do is give value to those components and because our target here are NOT the components I will mock them! [ref. code_example 2 line 22 – 25]:
val mockDataServices = mock[TDataServices] val mockDefaultControllerComponents = mock[DefaultControllerComponents] val mockActionBuilder = new JsonActionBuilder() val mockDefaultActionBuilder = new DefaultActionBuilder()
TDataServices is a trait so we need binding it to a Concrete class, that is a very good practice. As the play specification indicates: by default Play will load any class called Module that is defined in the root package (the “app” directory) or you can define them in any place and indicate where to find it in the play configuration file(application.conf) under play.modules configuration value.
.............. class Module extends AbstractModule { ................... bind(classOf[TDataServices]).to(classOf[DataServices]) .asEagerSingleton() } }
code_example 3 ref. source code bindings
You can find more information in Play Documentation about custom and eager bindings. You should pay attention about this, it is important for several practices and in our case we will see the benefits of it in Testing.
I recommend Test with Guice only when we do not have any other choice. That is why if I need to inject a data base connection, for example, I wouldn’t do it in the Controller it is better do it in the service class indeed.
Our first Test Testing Controllers begin in [ref. code_example 2 line 38 – 48] :
I need to simulate a behaviour so if any process call services.modelOfMatchFootball[ref.code_example 1 line 27] I will return Seq(matchGame):
when(mockDataServices.modelOfMatchFootball(“football.txt”)) thenReturn Seq(matchGame)
in my FootballControllerSpec.
In the same way I will mock the controller because I want to inject all mocked component too and test the flow of the controller. Remember that we are in [ref. code_example 2 line 38 – 48] so I have a Fake request , something important is that we can add to this request a header, body(json, text, xml), cookies and whatever that any request has. Test will execute Controller but with mocked components and will process everything and return a response in the same way:
......................... when(mockDataServices.modelOfMatchFootball("football.txt")) thenReturn Seq(matchGame) val request = FakeRequest(GET, "/football/matchs") .withHeaders(("Accept","application/json"),("Content-Type","text/plain")) val result = TestFootballleagueController.getMatchGame(request) val resultMatchGame: Match = (contentAsJson(result) \ "message" \ 0).as[Match] status(result) shouldBe OK resultMatchGame.homeTeam.name should equal(matchGame.homeTeam.name) resultMatchGame.homeTeam.goals should equal(matchGame.homeTeam.goals) .......................... |
If TestFootballleagueController.getMatchGame method had any parameter then the statement would be TestFootballleagueController.getMatchGame(anyparam)(request)[ref. line 5 previous code]. In our example I have to deal with content-negotiation so I have added a header easily to my FakeRequest and then we expect the right processing from my content-negotiation method and the right response.
It is important to know that FakeRequest(GET, “/football/matchs”) is the same that FakeRequest(GET, “/”), it has nothing to do with the routes defined in the conf/routes file.
In our Controller [ref.code_example 1] we are going to Test now the Actions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
package com.ldg.play.test import akka.stream.Materializer import com.ldg.basecontrollers.{BaseController, DefaultActionBuilder, JsonActionBuilder} import com.ldg.implicitconversions.ImplicitConversions.matchReads import com.ldg.model.Match import com.ldg.play.baseclass.UnitSpec import play.api.test.FakeRequest import play.api.libs.json._ import play.api.test.Helpers._ import play.api.http.Status.OK import play.api.inject.guice.GuiceApplicationBuilder import play.api.mvc.Results.Status import scala.io.Source class FootballActionsSpec extends UnitSpec { val jsonActionBuilder = new JsonActionBuilder() val defaultActionBuilder = new DefaultActionBuilder() val jsonGenericAction = new BaseController().JsonAction[Match](matchReads) val rightMatchJson = Source.fromURL(getClass.getResource("/rightmatch.json")).getLines.mkString val wrongMatchJson = Source.fromURL(getClass.getResource("/wrongmatch.json")).getLines.mkString implicit lazy val app: play.api.Application = new GuiceApplicationBuilder().configure().build() implicit lazy val materializer: Materializer = app.materializer /** * Test JsonActionBuilder: * * validate: content-type * jsonBody must be specific Model * * @see com.ldg.basecontrollers.JsonActionBuilder * * Request: application/json * */ "JsonActionBuilder with Content-Type:application/json and a right Json body" should "return a 200 Code" in { val request = FakeRequest(POST, "/") .withJsonBody(Json.parse(rightMatchJson)) .withHeaders(("Content-Type", "application/json")) def action = jsonActionBuilder{ implicit request => new Status(OK) } val result = call(action, request) status(result) shouldBe OK } ....... "JsonAction with Content-Type:application/json and a wrong Json body" should "return a 400 Code" in { val request = FakeRequest(POST, "/") .withJsonBody(Json.parse(wrongMatchJson)) .withHeaders(("Content-Type", "application/json")) def action = jsonGenericAction{ implicit request => new Status(OK) } val result = call(action, request) status(result) shouldBe BAD_REQUEST } ....... "DefaultActionBuilder with Content-Type:text/plain and a right Json body" should "return a 200 Code" in { val request = FakeRequest(GET, "/").withHeaders(("Accept","application/json"),("Content-Type", "text/plain")) def action = defaultActionBuilder{ implicit request => new Status(OK) } val result = call(action, request) status(result) shouldBe OK } ....... } |
code_example 4 ref. source code ActionsSpec
We are going to highlight this import [ref. code_example 4 line 10]:
The previous import will let call an action with an specific request(GET/POST….). An example of this call in [ref. code_example 4 line 48]:
But the previous call need an implicit value, an instance of a Materializer so the best way for build it if we can not get an instance of our application is through Guice see [ref. code_example 4 line 25 – 26].
I am testing the Actions so in my case I will test only Action/ActionBuilder and some aspects related with the request. I won’t test here the service that is executed under an specific action. Take a look about [ref. code_example 4 line 42] about our JsonBody in our FakeRequest.
It is not the objective of this post but you could take a look to JsonActionBuilder and DefaultActionBuilder [ref. code_example 4 line 19-20]
In our Controller [ref.code_example 1] we are going to Test now the Routes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.ldg.play.test import apirest.Routes import com.ldg.play.baseclass.UnitSpec import services.{MockTDataServices, TDataServices} import play.api.inject.bind import play.api.test.FakeRequest import play.api.test.Helpers._ import play.api.inject.guice.GuiceApplicationBuilder class FootballRoutesSpec extends UnitSpec { val app = new GuiceApplicationBuilder() .overrides(bind[TDataServices].to[MockTDataServices]) .configure("play.http.router" -> classOf[Routes].getName) .build() // implicit lazy val materializer: Materializer = app.materializer "Request /GET/football/PRML/matchs with Content-Type:text/plain and application/json" should "return a json file Response with a 200 Code" in { val Some(result) = route(app, FakeRequest(GET, "/football/matchs") .withHeaders(("Accept", "application/json"),("Content-Type", "text/plain"))) status(result) shouldBe OK } } |
code_example 5 ref. source code RoutesSpec
This aspect is not clear in PlayFramework documentations. First of all I need to create a Fake application with the same skeleton that the real so I need to do the following:
The previous 3 points are solved in [ref. code_example 5 line 15 – line 18]:
I have to import the auto-generated/compiled scala class Routes. PlayFramework constructs a Route class for each configuration route file. In our Routes class generated there is an important method:
def routes: PartialFunction[RequestHeader, Handler]
The aforementioned method can give us all route in our project. So if I want to test the routes of my project then a good idea should be make every request with his specific path and test if it is working properly.
Any way this post is just the tip of the iceberg. My advice is that take a look in the test module of playframework source code that is the case ScalaFunctionalTestSpec because the documentation in this point is not pretty well on playframework documentation.
So in examples like the next snapshot of code in framework documentation
1 2 3 4 5 6 7 8 |
"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") } |
They never explain how get applicationWithRouter in line 1 in the above code for instance. The solution:
1 2 3 4 5 6 7 8 |
val applicationWithRouter = GuiceApplicationBuilder().appRoutes { app => val Action = app.injector.instanceOf[DefaultActionBuilder] ({ case ("GET", "/Bob") => Action { Ok("Hello Bob") as "text/html; charset=utf-8" } }) }.build() |
Testing is easy and should be an style of programming, perhaps in lightbend the make it just a bit more difficult. Anyway, in my opinion, this play framework is terrific, not your documentation.