- 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.
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:
- Controller
- Actions
- Routes
- Our services or functionalities, obviously
- Encapsulate your operations in services.
- Inject the classes that manage the above services.
- Create the relationship Class - Trait. It will let you create binding in your tests with Guice in a more easy way and at the same time get the benefits already known of this programming practice.
- Mock services that you don't want to test. Your tests must be focused as much as you can in the functionalities that you want try.
- Test the Controller.
- Test the actions: When we talk about test actions that means test our action builders, test the definitions that process every path in each request. A good idea is that action builders can process all related with our request as for example content-type. It will let us filter for an expected Content-Type request and if it isn't what we expect stop the processing.
- Test the routes: It is important to know that all routes defined for us are working properly or at least can be reached by the proper path.
- Test any other Function or Service related with HTTP request. For example methods/functions related with content negotiation normally need make use of HTTP Header and in that case we will need set the header in a Fake-request.
- Test services functionalities. I won’t include anything related with HTTP request here. Just our own functionalities.
- Multimodule applications.
- Several route files.
- Several bindings class that are injected in Singleton Class.
- Class referencing configurations files.
- And for example you have bindings with Class that inject other Singleton class and that Class at the same time have some references to configuration files in your Project.
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:
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 | 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) } .............. } |
Some of my recommendations for the real life Controllers:
- I mostly need a custom action so inject a component [ref. code_example 1 line 1] in the previous snapshot of code [action: DefaultControllerComponents]) and this CUSTOM action will simplify the processing of your requests in Play and at the same time this design will make easy your test, something very important in our TDD process as we will see later.
- Encapsulate the operations that must be done in your actions in services and INJECT it in your controller([ref. code_example 1 line 1] [services: TDataServices]), some tips here: Inject a trait and bind that trait to a concrete class both things are truly important. If any service include for example any customizable object like a database connection do NOT inject at this level do it in the concrete class.
In our Controller [ref. code_example 1] we are going to Test first the Controller:
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]:
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.
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:
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:
code_example 4 ref. source code ActionsSpec
We are going to highlight this import [ref. code_example 4 line 10]:
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:
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 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
They never explain how get applicationWithRouter in line 1 in the above code for instance. The solution:
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.
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 | /** * 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 } } |
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() } }
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:
1 2 3 4 5 6 7 8 9 10 | ......................... 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 } ....... } |
We are going to highlight this import [ref. code_example 4 line 10]:
- import play.api.test.Helpers._
- call(action, request)
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 } } |
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:
- Create a fake app
- Mock all service that I am going to use like in my real app
- Create my route file
- [ref. code_example 5 line 15]: We create a mock using a binding. It was one of the important thing that explain at the beginning. Mocks implemented in Guice are completely different at Mockito. In Guice you will need to mock the whole object. So what we are doing here is overriding our binding with bindings to my mocked object. You can take a look of my mocked object MockTDataServices but in general you have to give fixed values in your mocked object to any definitions in your concrete class.
- [ref. code_example 5 line 16]: I am indicating here to my Fake app the route file that I want to use. The way to indicate to my fake app the route file is overriding the route file in our case play.http.router -> the router class indicate the route file of our app(the routes that we want to test)
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.
0 comentarios:
Publicar un comentario