Saturday, June 18, 2016

Unit testing in Play

Play testing may be done on different levels: unit tests, functional tests, server test etc. - as it is mentioned in Play testing manual. The focus in this post is on writing of unit tests.

Helpers and fakeApplication


Class Helpers is used a lot for unit tests. It imitates a Play application, fakes HTTP requests and responses, session, cookies - all whatever may be needed for tests. A controller under the test should be executed in a context of a Play application. The Helpers method fakeApplication provides an application for running tests. In order to use Helpers and fakeApplication a test class should derive from WithApplication.

The following Helpers API-s should be used:
Helpers.running(Application application, final Runnable block);
Helpers.fakeApplication();

Test with Helpers looks like this:
public class TestController extends WithApplication {
     @Test  
     public void testSomething() {  
         Helpers.running(Helpers.fakeApplication(), () -> {  
             // put test stuff  
             // put asserts  
         });
     }  
}  
Adding import statements for Helpers methods makes code more compact:
import static play.test.Helpers.fakeApplication;
import static play.test.Helpers.running;
...
     @Test  
     public void testSomething() {  
          running(fakeApplication(), () -> {  
               // put test stuff  
               // put asserts  
           });
     }
}  

Testing controllers


Let's call a controller method, which is bound to the particular URL in the routes as a "routed" method. An invocation of a routed method is called a controller action and has a Java type Call.
Play builds so-called reverse route to each action. Call to a reverse route creates an appropriate Call object. This reverse routing mechanism is used for testing controllers.

To invoke a controller action from test the following Helpers API should be used:
Result result = Helpers.route(Helpers.fakeRequest(Call action));

Controller tests example


1. The routes:
GET /conference/:confId    controllers.ConferenceController.getConfId(confId: String)  
POST /conference/:confId/participant controllers.ConferenceController.addParticipant(confId:String) 
2. Generated reverse routes:
controllers.routes.ConferenceController.getConfId(conferenceId) 
controllers.routes.ConferenceController.addParticipant(conferenceId)
3. The method getConfId is bound to GET and does not receive a body in a request. It may be invoked for test with:
Result result = Helpers.route(Helpers.fakeRequest(controllers.routes.ConferenceController.getConfId(conferenceId)));
4. The method addParticipant is bound to POST. It expects to receive a body in a request. Its invocation in test should be done like this:
ParticipantDetails inputData = DataSimulator.createParticipantDetails();
Call action = controllers.routes.ConferenceController.addParticipant(conferenceId);
Result result = route(Helpers.fakeRequest(action).bodyJson(Json.toJson(inputData));

Mocking with PowerMock


To enable mocking a test class should be annotated as following:
 @RunWith(PowerMockRunner.class)
 @PowerMockIgnore({"javax.management.*", "javax.crypto.*"})
 public class TestController extends WithApplication {
    ....

Mocking of a controller action


A controller call is mocked with RequestBuilder:
 RequestBuilder fakeRequest = Helpers.fakeRequest(action); 
For the above addParticipant an action is mocked with:
 RequestBuilder mockActionRequest = Helpers.fakeRequest(controllers.routes.ConferenceController.addParticipant(conferenceId)); 
To invoke the controller method:
 Result result = Helpers.route(mockActionRequest);
The test:
 @Test
 public void testLoginOK() {
     Helpers.running(Helpers.fakeApplication(), () -> {
          ///*whatever mocking*/Mockito.when(...).thenReturn(...);
          RequestBuilder mockActionRequest = Helpers.fakeRequest(
               controllers.routes.LoginController.loginAdmin());
          Result result = Helpers.route(mockActionRequest);
          assertEquals(OK, result.status());
     });
 }

Mocking of an action with JSON body


Let's suppose, that an input is an object of type T.
The action request mocking may be done in several ways.

Option 1:
 public static <T> RequestBuilder fakeRequestWithJson(T input, String method, String url) {  
      JsonNode jsonNode = Json.toJson(input);  
      RequestBuilder fakeRequest = Helpers.fakeRequest(method, url).bodyJson(jsonNode);  
      System.out.println("Created fakeRequest="+fakeRequest +", body="+fakeRequest.body().asJson());  
      return fakeRequest;  
 }  
Option 2:
 public static <T> RequestBuilder fakeActionRequestWithJson(Call action, T input) {  
      JsonNode jsonNode = Json.toJson(input);  
      RequestBuilder fakeRequest = Helpers.fakeRequest(action).bodyJson(jsonNode);  
      System.out.println("Created fakeRequest="+fakeRequest +", body="+fakeRequest.body().asJson());  
      return fakeRequest;  
 }       

Mocking of an action with Base authentication header


The action request mocking:
 public static final String BASIC_AUTH_VALUE = "dummy@com.com:12345";
 public static RequestBuilder fakeActionRequestWithBaseAuthHeader(Call action) {
      String encoded = Base64.getEncoder().encodeToString(BASIC_AUTH_VALUE.getBytes());
      RequestBuilder fakeRequest = Helpers.fakeRequest(action).header(Http.HeaderNames.AUTHORIZATION,
                                                 "Basic " + encoded);
      System.out.println("Created fakeRequest="+fakeRequest.toString() );
      return fakeRequest;
}

Mocking of an action with session


The action request mocking:
 public static final String FAKE_SESSION_ID = "12345";
 public static RequestBuilder fakeActionRequestWithSession(Call action) {
      RequestBuilder fakeRequest = RequestBuilder fakeRequest = Helpers.fakeRequest(action).session("sessionId", FAKE_SESSION_ID);
      System.out.println("Created fakeRequest="+fakeRequest.toString() );
      return fakeRequest;
}
The Play Session class is just an extension of the HashMap<String, String>. It may be mocked with simple code:
 public static Http.Session fakeSession() {  
      return new Http.Session(new HashMap<String, String>());  
 }  

Testing with Guice


See the dedicated post Using Guice for Play tests

Test configuration


Tests may run with a configuration different from an application configuration. See this post how to define a test configuration.

Unit tests execution


See the dedicated post Play unit tests execution

2 comments :

Henning said...

As for Fake Application replacement See https://www.playframework.com/documentation/2.6.x/api/java/play/test/FakeApplication.html
Please keep up the good work.

Ala said...

Thanks for the feedback.

About the author

My Photo
I trust only simple code and believe that code should be handsome. This is not a matter of technology, but professional approach, consolidated after years of software development. I enjoy to cause things working and feel very happy, when I manage to solve a problem.
Back to Top