Grails—Unit Testing View Rendering (with custom TagLib namespace quirk)

Grails, programming, test

Background

GroovyPageUnitTestMixin

From the Grails guide, testing a view in a unit test involves:

  • Annotating the Specification with: @TestMixin(GroovyPageUnitTestMixin).
  • Calling render(view: '...', model: ...) or render(template: '...', model: ...) whose returned value will be a String of the HTML output.

Custom TagLibs

Normally, without doing anything else, if the view or template uses custom (i.e. not ones from the g: namespace) TagLibs, they will not be expanded. For example, if a view GSP was like this:

<div id="blah">
    <abc:decorate code="${obj}" format="a" />
</div>

the rendered content for the TagLib will be pretty much verbatim of the original GSP. The only exception is that any ${...} will be evaluated. For example:

<div id="blah">
    <abc:decorate code="Object@3234f21" format="a" />
</div>

And from the Grails guide, using mockTagLib(DecorateTagLib) (assuming the TagLib class for the abc:decorate tag is implemented in the TagLib class DecorateTagLib) will then trigger the TagLib:

@TestMixin(GroovyPageUnitTestMixin)
class MyTestSpec extends Specification {
    ...

    void testBlah() {
        given: 
        mockTagLib(DecorateTagLib)

        expect:
        ...
    }
}

will now render something like:

 
<div id="blah">
    {output of DecorateTagLib.decorate}
</div>

Note that the same effect can be accomplished using the @Mock() annotation on the class:

@TestMixin(GroovyPageUnitTestMixin)
@Mock([DecorateTagLib])
class MyTestSpec extends Specification {
    ...

    void testBlah() {
        given: 
            ...

        expect:
        ...
    }
}

And Now the Quirk…

If there were multiple TagLib classes sharing the same namespace (abc in this example) used in the GSP:
DecorateTagLib.groovy

class DecorateTagLib {
    static namespace = 'abc'

    def decorate = { attrs -> ... }
}

and FormatTagLib.groovy

class FormatTagLib {
    static namespace = 'abc'

    def format = { attrs -> ... }
}

and the GSP uses them both:

<div id="blah">
    <abc:decorate code="${obj}" format="a" />
    <abc:format val="${x}" />
</div>

and only some of them was mocked:

@TestMixin(GroovyPageUnitTestMixin)
@Mock([DecorateTagLib])
// NOTE there is no mocking of the FormatTagLib class implementing abc:format
class MyTestSpec extends Specification {
    ...

    void testBlah() {
        given: 
            ...

        expect:
        ...
    }
}

then running the test will bring up and error for the TagLibs that are not mocked. For example, the error will be like:
Tag [format] does not exist. No tag library found for namespace: abc

To fix this scenario, either:

  • Mock all the TagLibs used in the GSP that share the same namespace (e.g. abc in these examples), or
  • Redistribute the TagLibs using unique namespaces so that they do not share the same namespace. However, note that the rule stays the same for all the individual namespaces:
    • When at least one TagLib from a namespace is mocked, ALL other TagLibs used in the GSP with that namespace must also be mocked.

Grails unit vs integration tests

Grails, programming, test

Services

Content here applies to Grails 2.1.3 and may also apply to newer versions.

Setup

We start with three service classes: OneService, TwoService, and ThreeService. OneService uses TwoService which uses ThreeService.

class ThreeService {
def hello() {
"Hello from Three"
}
}

class TwoService {
def threeService
def hello() {
"Hello from Two:" + threeService.hello()
}
}

class OneService {
def twoService
def hello() {
"Hello from One:" + twoService.hello()
}
}

Calling OneService#hello() would normally return "Hello from One:Hello from Two:Hello from Three"

Unit Test

Add a unit test for OneService:

@TestFor(OneService)
class OneServiceTests {
void testSomething() {
defineBeans {
twoService(TwoService)
threeService(ThreeService)
}
service.hello()
}
}

Run it and get this:

| Failure: testSomething(org.van.OneServiceTests)
| java.lang.NullPointerException: Cannot invoke method hello() on null object
at org.van.TwoService.hello(TwoService.groovy:7)
at org.van.OneService.hello(OneService.groovy:7)

The defineBeans{ ... } trick only works for one level, it seems. It got AService#bService hooked up, but BService#cService remained null.

So what happens is that OneService#hello() got to call TwoService#hello() OK. However, when TwoService#hello() tries to call ThreeService#hello(), we get a NPE because TwoService#threeService was not properly injected.

Integration Test

One way to test the flow is to use an integration test instead:

class OneServiceIntTestTests {

@Test
void testSomething() {
OneService oneService = new OneService()
TwoService twoService = new TwoService()
ThreeService threeService = new ThreeService()
twoService.threeService = threeService
oneService.twoService = twoService
oneService.hello()
}
}

While this works, we will need to do the injection plumbing ourselves.

Controllers

Setup

Add a TwoController class:

class TwoController {
def twoService

def index() {
render twoService.hello()
}
}

Accessing the controller returns Hello from Two:Hello from Three as expected.

Unit Test

Add a unit test for TwoController:

@TestFor(TwoController)
class TwoControllerTests {
void testSomething() {
defineBeans {
twoService(TwoService)
threeService(ThreeService)
}
controller.index()
}
}

And it fails similarly to OneServiceTests:

| Failure: testSomething(org.van.TwoControllerTests)
| java.lang.NullPointerException: Cannot invoke method hello() on null object
at org.van.TwoService.hello(TwoService.groovy:7)
at org.van.TwoController.index(TwoController.groovy:7)

Again, the defineBeans { ... } mechanism resolved TwoController#twoService. But once we call into TwoService#hello, it throws an NPE because TwoService#threeService is not properly injected.

Integration Test

And here’s the integration test for TwoController:

class TwoControllerIntTestTests {
@Test
void testSomething() {
TwoController twoController = new TwoController()
TwoService twoService = new TwoService()
ThreeService threeService = new ThreeService()
twoService.threeService = threeService
twoController.twoService = twoService
twoController.index()

assertEquals "Expected output", 'Hello from Two:Hello from Three', twoController.response.text
}
}

Summary

Unit tests, with their defineBeans { ... } mechanism, only helps inject one level of beans for the target (i.e. @TestFor()) class. If the injected beans used by that class then have more injected beans of their own, those are not injected correctly.

This works identically for services and controllers.

Integration tests allow for deeper chaining of injected beans. However, the construction/initialization and the actual injection have to be done manually.

Reference

Grails testing guide