At some point, akustik had a question about his approach in Play! and so asked for opinions on the mailing list. Turns out I had been over the same problems few weeks earlier at my (then) current project so offered to help. Since the answer is quite reusable I'm cross-posting here so that you can comment and discuss if my suggestion is actually correct. I worked may way into the solution to make sure I remembered all steps and the PR'd my code into the challenges github: https://github.com/scala-developers-bcn/challenges/pull/29
DISCLAIMER: I'm quite new to Play! and it's very possible there's a better way than what I suggest.
Dependency Injection and testing without Mocking in Play!
Play!'s Main Problem (TM) is the abuse of singleton objects for everything. While it's a great idea to create a single instance of anything and reuse it again and again it's also a problem to make everything object (scala equivalent for classes that contain only static methods). This object-based approach makes it difficult to inject instances at will.
Back to the trigger, here's akustik's question:
I wanted to change [the controller] implementation for testing purposes without modifying the code nor changing to a variable and adding a setter.To which I could only reply:
You hit one of the big problems of Play! testing: there's no easyAnd finally found few minutes to properly solve the problem.
recommendation for unit testing. The path of objects and Integration
Testing is the default suggestion. Sad.
Dependency Injection
To escape the object singleton mess which forbids you from doing plain old DI you first have to convert you controllers from objects to classes.
(I'm not 100% of what I'll explain now)
But once you did that you have to prepend your routes mappings with an '@' sign. That causes you HTTP requests to request an instance to Play! The responsible to resolve the required instance is a thing** called GlobalSettings.
(back to 100% sure)
Then you have to create your subclass of play.GlobalSettings and then go to application.conf and specify what your Global class is. But you class controllers still use singleton repositories. Let's make the repos injectable. Add a constructor parameter to your controller:
class MyCtlr(repo:MyRepo) extends Controller
And now you made your controller unavailable because Play can't resolve the incoming requests. Here's where you go back to your Global class and override the method getControllerInstance so that any incoming request lets you decide what Controller to use. And there you have it!
Your Global soon becomes this:
def flightRepo = new InMemFlightsRepository
def flightCtlr = new FlightsCtlr(flightRepo)
override def getControllerInstance[A](
// extend here when adding more controllers... probably a pattern matching.
flightCtlr.asInstanceOf[A]
}
So far so good. We got ourselves so DI wonders.
(trolling mode on)
It's at this point where your Global should be renamed ApplicationContext and you should consider your:
def flightCtlr = new FlightsCtlr(flightRepo)
a synonim of: (blogspot won't let me use lt and gt)
bean id=flightCtlr class=controller.
constructor-arg="flightRepo"
You could have also done:
val flightCtlr = ...
which would be:
bean id=... class=... scope=singleton
Scala 1 - XML fuck you!... I mean 0
(trolling mode off)
Unit Testing (without mocking)
And so we end up on our wonderful test.
If you've read the Play docs you probably have a test that looks like this:
"return 200 on flights/" in {
running(FakeApplication()) {
val flights = route(FakeRequest(GET, "/flights")).get
...
Well, without you knowing it uses your Global class. The final trick to easily inject your Mock classes is to tell your Fake Application to use a different Global:
running(FakeApplication( withGlobal= Some(MockingGlobal() ) )) {
And that's it!
Well, no!
Oh God this is horrible!
** Remember when I said "a thing** called GlobalSettings" , well by thing I mean some bytecode that may be class or trait and I was being vague on purpose because there's both a class and a trait called
GlobalSettings and you'll need to use both to achieve DI and testing. To distinguish which GlobalSettings you are using pay close attention at the package.
- To override Global for you app extend play.GlobalSettings
- To inject a MockGlobal into FakeApplication when testing mixing play.api.GlobalSettigns
So, to make your testing work you have to provide a MockingGlobal of yours which must mix-in play.api.GlobalSettings. I chose reusing my original Global class and just override a faked repository.
Again, while the running environment requires you to provide a subclass of play.GlobalSettings, FakeApplications expects an Option[play.api.
I just go and extend and mixin to keep all my DI in a single place:
class MyGlobal extends play.GlobalSettings
with play.api.GlobalSettings { ...
and finally:
val globalForTest = new Global {
override def flightRepo = new InMemFlightsRepository {
override def loadAll(): List[Flight] =
List(Flight("ON_TIME","BOS", "CHG", "19B"))
}
}
Conclusion
Now, after this terrible mess lets puts this in context (no pun intended) and compare this with the 'easy' setup of a Spring-MVC app with testing and everything.
It's not much simpler but it's 100% scala so it must be cooler.
Cap comentari:
Publica un comentari a l'entrada