playframework / playframework

The Community Maintained High Velocity Web Framework For Java and Scala.
http://www.playframework.com
Apache License 2.0
12.54k stars 4.09k forks source link

How to access application components in functional tests while using compile time DI? #8099

Closed Dasiu closed 6 years ago

Dasiu commented 6 years ago

API: Scala, Play: 2.6.X

My need is to write functional test, which refers to application component with compile time DI and with server and whole application running. After reading docs, I'm not sure what is proper way to achieve that. I've learned, that I should use injector in order to access application components (including business components) like this:

class ExampleSpec extends PlaySpec with GuiceOneServerPerSuite {

  // Override app if you need an Application with other than
  // default parameters.
  override def fakeApplication(): Application =
    new GuiceApplicationBuilder().disable[EhCacheModule].router(Router.from {
      case GET(p"/") => Action { Ok("ok") }
    }).build()

  "test server logic" in {
    val wsClient = app.injector.instanceOf[WSClient]
    val myPublicAddress = s"localhost:$port"
    val testPaymentGatewayURL = s"http://$myPublicAddress"
    // The test payment gateway requires a callback to this server before it returns a result...
    val callbackURL = s"http://$myPublicAddress/callback"
    // await is from play.api.test.FutureAwaits
    val response = await(wsClient.url(testPaymentGatewayURL).withQueryString("callbackURL" -> callbackURL).get())

    response.status mustBe OK
  }
}

https://www.playframework.com/documentation/2.6.x/ScalaFunctionalTestingWithScalaTest

Intuitively, I presumed, that for compile time DI it can be done in the same way, of course, without additional magic, programmer needs to set up such injector himself - explicitly. My attempt to do that failed, because BuiltInComponents.injector is lazy and following line do not compile:

override val injector = new SimpleInjector(super.injector) + ??? /* additional components */

with error: "super may not be used on lazy value injector". Call to super is needed, since BuiltInComponents contains some basic dependencies, which are probably needed by an application.

After facing that I fell back to another solution. Underneath playframework's test utilities initializes app variable for each test. The variable is initialized based on WithApplicationComponents.components definition. components def is also available in test's body, so my idea was to call it in order to access components required by test, like here:

"dummy test" in {
  components.myDummyComponent
}

it actually works but it looks like fragile thing, because components is a definition, so each call creates different BuiltInComponentsFromContext - different to create application for test and different to access application component.

Given all that, my questions are:

My current test infrastructure is a little bit more complicated, it can be found here, if needed: https://github.com/Dasiu/realworld-starter-kit/tree/master/test

gmethvin commented 6 years ago

@Dasiu It seems like you're actually talking about https://github.com/playframework/scalatestplus-play, rather than playframework itself. The core issue I believe is that you want something like components to access at the class level, but you want to make sure that you're using the same exact instance used by the running application.

The first solution you mentioned seems like an ugly hack. If you're using compile-time DI, ideally you want to access the components in a type-safe way. The best way to do that is directly using the actual components class you used to create the application. Yes, it would be possible to implement an injector that inspects the components class at runtime and calls the right method, but there's no good reason to add another level of indirection.

As far as I can tell this is not a bug in Play at all so I'm closing this, but I opened https://github.com/playframework/scalatestplus-play/issues/108 in scalatestplus-play that I think explains the main problem you're describing. That also describes an idempotent way to access the components for the running application.