Attempting to test a Controller class here. I’m mocking the services that it calls, but I’m having trouble with the test setup for all that’s “above” the controller. I’ve created a fakeApplication() and created a fakeRequest(), but my test is failing because it can’t find the values for the environment variables that the application requires. It doesn’t matter what they are for this test, because all the bits that need them are mocked, but I do need to specify something so that the test will run. What am I missing?
Sounds like you want a test class for your unit test(s) that extends WithServer
And then override provideApplication
, where you can do something like:
return new GuiceApplicationBuilder()
.configure(<key>, <value>);
Documentation:
https://www.playframework.com/documentation/2.6.x/JavaFunctionalTest
https://www.playframework.com/documentation/2.6.x/ScalaFunctionalTest
https://www.playframework.com/documentation/2.6.x/JavaTestingWithGuice
https://www.playframework.com/documentation/2.6.x/ScalaTestingWithGuice
Perhaps you can explain - if I only want to test my Controller, divorced from all other code, why do I need to run it with a server? Ideally I’d just create an instance of my Controller, and that would be it, but Controllers in Play require that there be a request backing up the particular action. If I could create a request without creating the application, that would be ideal, but I don’t know how to create a request without creating an application. And now I need to have the whole server running?
At the base level, I just want to unit test my controller, and it seems like I should be able to do that without all the extra infrastructure. Is there a way to do that?
My suggestion comes from recently doing this myself. Perhaps there is a better way I’m not aware of.
I see my mistake now, I was a bit too quick. You mention it can’t find environment variables during the test. Can it find them when normally running the application in DEV mode?
Because that sounds a bit strange. An environment variable, far as I know, is something visible to your entire OS.
For instance, from the commandline:
export MY_VAR=myEnvironmentVariable
And those should pretty much always be reachable, regardless of using WithServer, WithApplication, something else, or the mode you run your application in.
So, is that what you’re referring to, or is it something else? Because I first thought you meant the values from the <yourconffile>.conf
file that every application uses. Which is why I suggested the construct with providing the applicationbuilder, using .configure
to pass in key-value pairs to the configuration of the fake application.
I think I see my error. I’m trying to run the test the way I’m used to running tests from Eclipse. In this case, I should be running via sbt and using the local.conf file that I’ve specified for local development.
But I guess I’m a little stubborn. I shouldn’t have to run an application just to unit test my controller. I just want to test the error responses without any connection anywhere to any server. I want to mock a request, have the controller do its thing, and test the output. Is there a way I can do this in Play? Obviously I can unit test a service (referenced from the controller) because I have control to all the inputs, but controllers have this thing where the request is not passed in to the method that provides the implementation.
I recall trying it back when I first started out, but just using RequestBuilder in the end.
If you’re using Play with routes, then you’ll want something to interpret those calls being made. I’ve never found a way to mock that, so I usually use WithServer
and run like that.
Perhaps the developers know more?
I agree, ideally it should be possible to test your controller with only its direct dependencies, without having a full app. This is completely possible with the Scala API (and the low-level EssentialAction
API) currently, but in the Java API the situation is a bit more complicated.
First, to answer your question: there’s no Play-provided testing API to allow you to call Java action without going through an application. No server is required, just an application instance. If you read the Java functional testing docs you’ll see you can use the route
helper. You can use a FakeRequest
instead of a real request there as shown in the examples.
There are a few reasons why you can’t call Java actions directly. Play keeps request state in thread local context (used by request()
and other methods), which needs to be set before the action is invoked. In addition, Play uses annotations to wrap the action in other behavior (“action composition”). The original designers of Play did it this way because it makes the API much more succinct and familiar to Java developers, though it makes unit testing harder. To “call” your action, Play needs to wrap your method with special code to set things up, which requires various other components from the application.
As Play evolved, its focus shifted more to testability and eliminating global state. I’m sure it would be possible to build unit testing tools for the current action API (e.g. Play’s integration tests have this code for mocking Java actions), but since this wasn’t a major user request we decided it was better to focus on improving the API for Java actions so they’re inherently more testable.
There is some discussion about removing the thread local context, and that should happen in an upcoming Play release. Making Java action composition easier to do in unit tests will depend somewhat on how the first issue is solved.
I have basically solved this by separating the testable code out:
public CompletionStage<Result> getFoo(String key) {
return formResponse(() -> getUsers(getRequest(), key));
}
protected Result getFoo(Request request, String key)
throws Exception {
authenticate(request);
validateRequest(request);
return ok(Json.toJson(service.getFoo(key)));
}
In this way, I can actually unit test the protected method, which does everything other than exception handling (and I can test the exception handling separately).
This isn’t perfect, but it’s pretty good for my purposes.