I love Groovy’s meta-programming capabilities. Duck typing, DSLs, ExpandoMetaClass… it’s all great until you blow your arm off with them and spend two hours picking the pieces back up. Such was my experience today as I was trying to set up my first unit test for a Grails controller. I needed to pass in some parameters. As I just wrote in my last post, the grails user guide has information on integration testing controllers, but nothing more than a brief mention of the ControllerUnitTestCase. Unfortunately, searching for phrases like “grails unit test controller” on Google turn up posts from last Fall, before the ControllerUnitTestCase was released with Grails 1.1. Don’t ask me why I didn’t just search for ControllerUnitTestCase from the start. 20/20 hindsight.
Working with what I could find, I came across a blog post from March 2008 that covered integration testing with controllers, much like the grails User Guide. It at least had some explicit examples of how to pass in parameters, so I hoped the approach would work with my ControllerUnitTestCase implementation. Using the code snippets in the post as a guide, I setup my test something like this:
controller.request.params = [layoutType:'filmstrip',compKey:'testKey']
controller.create()
This looks good, right? The test scaffolding connects the injected params property of the controller with the params property of the request. Should work like a charm.
And so started my descent into WTF Land.
The tests compiled and ran fine, except that no params were getting passed through to the controller. I assumed that the old documentation for integration testing no longer exactly applied for the new unit test setups. Eventually I dug through enough of ControllerUnitTestCase, MvcUnitTestCase, and MockUtils that I figured out there was a way to get direct access to the params property of the controller. “Makes sense,” I thought, “sort of a mini-mock. Who needs the request for a unit test?” Great, so let’s try this:
controller.params = [layoutType:'filmstrip',compKey:'testKey']
controller.create()
Run again and…
groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: params for class: YourController
Erk. Closer inspection of grails.test.MockUtils.addCommonWebProperties() reveals how the params property is added to the controller class with only a get method. Okay, so we can still manipulate the map once we get it. One more try:
controller.params.putAll([layoutType:"filmstrip",compKey:'testKey'])
controller.create()
Success! Finally, information is going where it needs to be. I later realized that section 9.2 of the user guide does actually show an example similar to this, except that it sets each item on params individually:
def controller = new AuthenticationController()
controller.params.login = "marcpalmer"
controller.params.password = "secret"
controller.params.passwordConfirm = "secret"
controller.signup()
“Oh well,” I say to myself, “at least I got it working. I’d better post a comment on that old blog post so that others know the new way to set parameters.” I returned to the blog page, moved down to the comments section… and saw that someone else had posted a comment just last month thanking the author for the good information. Wait a second… you normally don’t thank someone if their examples don’t work. So I scrolled up and took a closer look at the examples again:
controller.request.parameters = [sender:'me',msg:'test']
controller.create()
Anyone with a better eye for detail than me see the difference that sent me down the rat hole? It’s request.parameters, not request.params as I had entered. For the 100th time today, Doh!
But wait a second (or an hour, as the case may be)… If I was calling request.params and there is no params property, how did my test run at all? Dig, dig, dig… It turns out that the request is a GrailsMockHttpServletRequest object. Scanning through that class, I see the ever so powerful and ever so dangerous setProperty(String name, value) override. If you try to set a property that doesn’t exist, it adds the value as an entry in the request’s attributes map. So the test scaffolding was perfectly happy with my request.params call. It nicely tucked my params map away in the attributes and continued on its way.
Sigh…it almost makes me miss compile-time method checking… but not quite.