[Play 2.4] Unit testing controllers methods that expect an http request body

2,262 views
Skip to first unread message

Evan Lennick

unread,
Jul 5, 2015, 3:10:38 PM7/5/15
to play-fr...@googlegroups.com
I have been trying to figure out if there is a way to minimize the amount of stuff being tested when writing unit tests against controllers. This is obviously very easy to do in situations where all the controller method only expects is a parameter being passed into it like the example under "Unit testing controllers" on this page:


It that case it is literally possible to just instantiate the controller POJO using Guice or "new" call the method to test with a test parameter. However if I want to test a controller method that expects some sort of http request body (ie: a majority of POST requests) and it accesses the request() object, I have to resort to writing a router test that is much more heavy weigh and spins up a fake application and passes through all the controller Action's, etc. 

So my question is, are there any mechanisms built into play or ways of writing my controllers that would allow me to test controller methods as actual unit tests and in a very lightweight fashion even if they require a request body? Or in that case is it mandatory to mock out a fake request and use Http.RequestBuilder and spin up a fake application, etc?

Just to try and give an example and be clear... here is how I am currently able to test a simple GET that only expects some query parameters that are passed into the controller as a parameter:

    @Inject
   
private ControllerToTest controllerToTest;

   
@Test
   
public void testControllerMethod() {
       
Result result = controllerToTest.index(someParam);
        assertThat
(result.status()).isEqualTo(OK);
   
}

How could I go about testing all implementations of controllers this simply, if it is possible at all? Thanks!

-Evan

James Roper

unread,
Jul 8, 2015, 8:31:21 PM7/8/15
to play-framework
Hi Evan,

Let me first explain the reasons why it's hard and what we're doing to address that.

Firstly, Java actions depend on thread locals to access anything about the request.  So, in order to test, you need to setup those thread locals. We're investigating a number of ways to address this, one is to make Java actions more like Scala actions where the action method doesn't directly execute the action, but rather builds and returns the action to handle the request.  Another idea that literally came to me last night was to introduce a syntax in the routes file where you can declare a request parameter, eg @request, and the request will be passed in there, so you don't need to rely on thread locals.

Secondly, the Play request object depends on global state, for example, to parse the session.  Without a started application, accessing certain things in the request could blow up.  We're addressing this in Play 3 by making request objects dumb immutable objects, rather than live objects that process things on request.  So the session will be parsed and then passed to the request, rather than the request object itself parsing it - while this means each request requires more work up front to create it, this should actually be a performance benefit because the session doesn't tend to change a lot between requests on the same connection, so that means we can cache it on the connection and only reparse when it changes.

Thirdly, and this is the biggest problem I think at the moment, and that is that the body parser, and other action composition that may be necessary to be invoked before your request is invoked, depends on an application, at a minimum to instantiate them using Guice. Addressing this is difficult, as long as we continue to use annotations to specify body parsers and action composition, there's not a lot of way around this. The builder approach to building actions mentioned before however would address this, as it means action composition and body parsers would be specified in the builder, so anything the body parsers/actions needed could be injected, and mocked if necessary in tests.  The @request syntax idea may also present some options there, but that still needs to be fleshed out.

So, after all that, here's the simple answer to your question:

import play.test.Helpers;

Result result = Helpers.invokeWithContext(
    Helpers.fakeRequest().bodyJson(...),
    () -> controllerToTest.someAction(...);
)

This will generally work, but due to everything mentioned above is limited, for example, you may still run into problems with no started application, and you may have problems with action methods not being invoked.

Cheers,

James

--
You received this message because you are subscribed to the Google Groups "play-framework" group.
To unsubscribe from this group and stop receiving emails from it, send an email to play-framewor...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/play-framework/5e816798-73ae-46df-ba9c-e69de970a633%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
James Roper
Software Engineer

Typesafe – Build reactive apps!
Twitter: @jroper

Evan Lennick

unread,
Jul 8, 2015, 9:22:44 PM7/8/15
to play-fr...@googlegroups.com
One more small question about your example... is invokeWithContext() supposed to be the only non-static method in Helpers.java? It cannot actually be invoked statically like in your example. However if I do the following it works fine:

        Helpers helpers = new Helpers();
       
Result result = helpers.invokeWithContext(
               
Helpers.fakeRequest().bodyText("some request body here"),
                controllerToTest
::postRequest);

Regardless, thank you for the very thorough answer, it was very helpful!

-Evan

James Roper

unread,
Jul 9, 2015, 11:23:17 PM7/9/15
to play-framework
On 9 July 2015 at 11:22, Evan Lennick <elen...@gmail.com> wrote:
One more small question about your example... is invokeWithContext() supposed to be the only non-static method in Helpers.java?

No, that's a bug.  Please raise an issue to fix and document the use of invokeWithContext.
 

For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages