2012-12-24

Testing Grails Filters that use Services

For SourceNode, I was writing a filter to redirect the user when the application is in "configuration-only" mode, as controlled by the AdministrationService. Let's say the code looks something like this:

grails-app/controllers/blogfiltertest/BookController.groovy:

package blogfiltertest

class BookController {
    def index() { }
}

grails-app/controllers/blogfiltertest/InitialConfigurationController.groovy:

package blogfiltertest

class InitialConfigurationController {
    def configurationRequired() {}
}

grails-app/services/blogfiltertest/AdministrationService.groovy:

package blogfiltertest

class AdministrationService {
    static transactional = false

    boolean isConfigurationOnly() {
        return false
    }
}

grails-app/conf/blogfiltertest/AdministrationFilters.groovy:

package blogfiltertest

class AdministrationFilters {
    def administrationService

    def filters = {
        configOnly(controller: 'initialConfiguration', invert: true) {
            before = {
                if (administrationService.isConfigurationOnly()) {
                    redirect(controller: 'initialConfiguration', action: 'configurationRequired')
                    return false
                } else {
                    return true
                }
            }
        }
    }
}

I wanted to write a unit test for this filter. It started something like this:

test/unit/blogfiltertest/AdministrationFiltersTest.groovy:

package blogfiltertest

import grails.test.mixin.*

@TestFor(BookController)
@Mock(AdministrationFilters)
class AdministrationFiltersTests {
    void testConfigurationOnlyRedirected() {
        withFilters(controller: 'book', action: 'index') {
            controller.index()
        }
        assert response.redirectedUrl == '/initialConfiguration/configurationRequired'
    }
}

The first roadblock I ran into was this:

java.lang.NullPointerException: Cannot invoke method isConfigurationOnly() on null object

This appears to be a Grails bug, GRAILS-8976. At least as of Grails 2.1.2, Grails unit tests don't properly perform dependency injection into filters. So, a workaround: manually have the filter get the appropriate service from the application context. This will also be run at run-time, resulting in a double-lookup of the bean definition. Here's the patched vesion:

package blogfiltertest

class AdministrationFilters {
    def administrationService

    def filters = {
        configOnly(controller: 'initialConfiguration', invert: true) {
            before = {
                administrationService = applicationContext.getBean('administrationService')
                if (administrationService.isConfigurationOnly()) {
                    redirect(controller: 'initialConfiguration', action: 'configurationRequired')
                    return false
                } else {
                    return true
                }
            }
        }
    }
}

It appears that the bean lookup needs to happen in the before closure. If you put it directly in filters, it doesn't have access to the applicationContext, and if you put it in configOnly, it won't find beans defined in the test.

Now there's a different error:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'administrationService' is defined

That's because, as per the documentation in the section "Testing Spring Beans", only a subset of the Spring beans are available for testing use, and you use defineBeans to make more available. Here's the updated version of the test using Grails mocking support to inject a mock version of the service.

package blogfiltertest

import grails.test.GrailsMock
import grails.test.mixin.*

@TestFor(BookController)
@Mock(AdministrationFilters)
class AdministrationFiltersTests {
    void testConfigurationOnlyRedirected() {
        defineBeans {
            administrationServiceControl(GrailsMock, AdministrationService)
            administrationService(administrationServiceControl: "createMock")
        }

        def administrationServiceControl = applicationContext.getBean("administrationServiceControl")
        administrationServiceControl.demand.isConfigurationOnly { true }

        withFilters(controller: 'book', action: 'index') {
            controller.index()
        }
        assert response.redirectedUrl == '/initialConfiguration/configurationRequired'
    }
}

When I ran into errors like below, it meant that I was trying to do bean lookup by type, which doesn't seem to be working with this style of mocking. The fix was to do lookup by name (which is more consistent with how Grails will do the injection anyway).

org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [blogfiltertest.AdministrationService] is defined: expected single bean but found 0:

Update 12/12/2012: If your filter uses URI scope (even if it's for all URIs) rather than controller/action, you'll need to set a URI on the request in the test for it to match using withFilters. Here's an example of what it looks like:

request.forwardURI = '/book/index'

2012-12-14

Grails Release Plugin NoClassDefFoundError: groovyx.net.http.HTTPBuilder

A few days ago, I was trying to release an update to the grails-page-resources plugin, when I ran into an error:

...
Notifying plugin portal 'http://grails.org/plugin/page-resources' of release...
| Error Error executing script PublishPlugin: groovyx.net.http.HTTPBuilder
java.lang.NoClassDefFoundError: groovyx.net.http.HTTPBuilder
    at PublishPlugin$_run_closure1.class$(PublishPlugin)
    at PublishPlugin$_run_closure1.$get$$class$groovyx$net$http$HTTPBuilder(PublishPlugin)
    at PublishPlugin$_run_closure1.doCall(PublishPlugin:421)
    ...

Searching Google for help didn't seem to turn up anything helpful, nor did turning on additional output with --stacktrace --verbose. At the time, this was already with the most recent version of grails-release-plugin, 2.2.0. So, I submitted a JIRA ticket, GPRELEASE-40.

Yesterday, I had the bright idea of checking the Grails mailing lists to see if there was anything useful there. And sure enough, the answer.

What it boils down to is that Grails wasn't actually running the specified version of the plugin, and some arcane steps were needed to get it to do so.

I love Grails, but my biggest frustration with it is when I run into one of these mystical incantations that are needed without a readily apparent reason, and not found in the otherwise great documentation, but rather only by stumbling upon them elsewhere.

For now, I'm going with a setting recommended by Peter Ledbrook here:

grails.project.work.dir = "target/$grailsVersion"