Marcin Erdmann

Groovy, Grails, Geb...

Mocking g.render() tag in Grails 2

With the addition of @TestFor and test mixins unit testing of taglibs has been completely changed in Grails 2. One of things that changed is testing custom taglibs that use default Grails tags, for example g.render(). Beforehand your tests would throw errors if you haven't mocked it, but now a call to g.render() will behave as if in a running application - pick up the specified template from grails-app/views and render it. Sometimes it is not what you really want.

Let's say that we are creating a taglib to render handlebars templates. Given the following template located in grails-app/views/handlebars/myTemplate.gsp:

<h1>{{title}}</h1>

and a following taglib call in our view:

<handlebars:template name="myTemplate" />

based on conventions we expect the following result:

<script id="myTemplate" type="text/x-handlebars-template">
    <h1>{{title}}</h1>
</script>

Our taglib will simply render the template from grails-app/views/handlebars with a given name and wrap it with a script tag of a hanldebars template specific type. But how do we test it? We don't want to add a template to grails-app/views/handlebars just for test purposes.


UPDATE: You should probably ignore the following and see the follow up post as Rob Fletcher has pointed out to me a much simpler way of doing it.

After having a quite lengthy look at implementation of g.render() I've noticed that it uses GrailsConventionGroovyPageLocator#findTemplateInBinding() which returns a GroovyPageScriptSource instance to load scripts. The locator used comes from groovyPageLocator field of GroovyPagesTemplateRenderer bean. Knowing that we can now easily mock the script locator in our Spock specification:

@TestFor(HandlebarsTagLib)
class HandlebarsTagLibSpec extends Specification {

    GrailsConventionGroovyPageLocator mockScriptLocator
    GroovyPageScriptSource mockScript

    def setup() {
        mockScriptLocator = Mock(GrailsConventionGroovyPageLocator)
        mockScript = Mock(GroovyPageScriptSource)

        GroovyPagesTemplateRenderer renderer = applicationContext.getBean(GroovyPagesTemplateRenderer)
        renderer.groovyPageLocator = mockScriptLocator
    }

    private void mockGspTemplateContents(String path, String contents) {
        mockScript.URI >> new URI(new Date().time.toString())
        mockScript.suggestedClassName() >> 'pageName'
        mockScript.scriptAsString >> contents
        mockScriptLocator.findTemplateInBinding(_, path, _) >> mockScript
    }

    private void mockGspTemplateMissing(String path) {
        mockScriptLocator.findTemplateInBinding(_, path, _) >> null
    }
}

There are some calls to getURI() and suggestedClassName() on the script that also have to be mocked for the technique to work. Note that we are just returning dummy value for page name as otherwise we end up with a NullPointerException and we want the script URI to be unique as otherwise it might get pulled from a cache.

Now we can write our features - one for an existing template and one for a missing template:

@TestFor(HandlebarsTagLib)
class HandlebarsTagLibSpec extends Specification {

    (...)

    def 'jsTemplate renders script tag with template'() {
        given:
        def templateName = 'someTemplate'
        mockGspTemplateContents("/handlebars/${templateName}", '<div>template content</div>')

        when:
        String rendered = applyTemplate("<handlebars:template name=\"${templateName}\" />")
        GPathResult result = new XmlSlurper().parseText(rendered)

        then:
        result.name() == 'script'
        result.@type == HANDLEBARS_TAGLIB_MEDIA_TYPE
        result.@id == templateName
        result.div.text() == 'template content'
    }

    def 'exception is thrown when js template is not found'() {
        given:
        def templateName = 'doesNotExist'
        def templatePath = "/handlebars/${templateName}"

        mockGspTemplateMissing(templatePath)

        when:
        applyTemplate("<handlebars:template name=\"${templateName}\" />")

        then:
        GrailsTagException exception = thrown()
        exception.message.startsWith("Template not found for name [${templatePath}]")
    }
}

Having the tests in place we can now write our implementation:

class HandlebarsTagLib {
    static namespace = 'handlebars'

    static final HANDLEBARS_TAGLIB_MEDIA_TYPE = 'text/x-handlebars-template'

    def template = { attrs, body ->
        new MarkupBuilder(out).script(type: HANDLEBARS_TAGLIB_MEDIA_TYPE, id: attrs.name) {
            mkp.yieldUnescaped(g.render(template: "/handlebars/${attrs.name}"))
        }
    }
}