Marcin Erdmann

Groovy, Grails, Geb...

Testing Grails controller actions that use bindData method and render validation errors as JSON

Let’s assume that we have a controller action in a grails application that updates a user domain instance. We want name and surname to be updateable but login and password shouldn’t be updateble. To achieve that we will use the controller’s bindData() method with whitelisting. The action will be called using ajax so we also want to render JSON with validation errors in response if there were any. So our controller might look like that:

package nl.jworks

import grails.converters.JSON

class UserController {

    def update = {
        User user = User.get(params.id)

        bindData(user, params, [include: ['name', 'surname']])
        if (user.validate()) {
            user.save()
        } else {
            render user.errors as JSON
        }
    }
}

And the example User class might look like that:

package nl.jworks

class User {
    String login
    String password
    String name
    String surname

    static constraints = {
        login blank: false
        password blank: false
        name blank: false
        surname blank: false
    }
}

Our next step will be unit testing the action. We want to make sure that only the whitelisted fields are copied from parameters (testUpdate) and also that the validation errors are rendered (testUpdateValidationError):

package nl.jworks

import grails.test.ControllerUnitTestCase
import grails.converters.JSON

class UserControllerTests extends ControllerUnitTestCase {
    User user

    protected void setUp() {
        super.setUp()
        user = new User(login: 'login', password: 'pass', name: 'name', surname: 'surname')
        mockDomain(User, [user])
    }

    void testUpdate() {
        controller.params.id = user.id
        controller.params.login = 'new login'
        controller.params.password = 'new password'
        controller.params.name = 'new name'
        controller.params.surname = 'new surname'

        controller.update()
        user.with {
            assert login == 'login'
            assert password == 'pass'
            assert name == 'new name'
            assert surname == 'new surname'
        }
    }

    void testUpdateValidationError() {
        controller.params.id = user.id
        controller.params.name = ''

        controller.update()
        def response = JSON.parse(controller.response.contentAsString)
        response.errors.with {
            assert size() == 1 && first().field == 'name'
        }
    }
}

It turns out that those tests won’t pass. But not because of the fact that we have bugs in the code, it’s just that grails doesn’t do enough magic for our controller action to be unit testable out of the box.

First problem is that the bindData() method is not mocked in ControllerUnitTestCase. Fortunately thanks to this stackoverflow response we know how to easily mock it. To make it reusable let’s create a mixin out of it:

package nl.jworks.mixin

import grails.test.ControllerUnitTestCase
import org.codehaus.groovy.grails.web.metaclass.BindDynamicMethod

@Category(ControllerUnitTestCase)
class MockBindDataMixin {
    void mockBindData() {
        def mc = controller.metaClass
        def bind = new BindDynamicMethod()
        mc.bindData = { Object target, Object args ->
            bind.invoke(controller, "bindData", [target, args] as Object[])
        }
        mc.bindData = { Object target, Object args, List disallowed ->
            bind.invoke(controller, "bindData", [target, args, [exclude: disallowed]] as Object[])
        }
        mc.bindData = { Object target, Object args, List disallowed, String filter ->
            bind.invoke(controller, "bindData", [target, args, [exclude: disallowed], filter] as Object[])
        }
        mc.bindData = { Object target, Object args, Map includeExclude ->
            bind.invoke(controller, "bindData", [target, args, includeExclude] as Object[])
        }
        mc.bindData = { Object target, Object args, Map includeExclude, String filter ->
            bind.invoke(controller, "bindData", [target, args, includeExclude, filter] as Object[])
        }
        mc.bindData = { Object target, Object args, String filter ->
            bind.invoke(controller, "bindData", [target, args, filter] as Object[])
        }
    }
}

Now all that we have to do to make it work is to apply it on our test class and call the mockBindData() method in the setUp() method.

Second problem is that by default the JSON converter doesn’t know how to convert bean validation errors. But that’s easily fixable as well – all we have to do is register validation error marshaller in the testUpdateValidationError test:

JSON.registerObjectMarshaller(new ValidationErrorsMarshaller())

After the aforementioned changes our passing test class should look like that:

package nl.jworks

import grails.test.ControllerUnitTestCase
import nl.jworks.mixin.MockBindDataMixin
import grails.converters.JSON
import org.codehaus.groovy.grails.web.converters.marshaller.json.ValidationErrorsMarshaller

@Mixin(MockBindDataMixin)
class UserControllerTests extends ControllerUnitTestCase {
    User user

    protected void setUp() {
        super.setUp()
        user = new User(login: 'login', password: 'pass', name: 'name', surname: 'surname')
        mockDomain(User, [user])
        mockBindData()
    }

    void testUpdate() {
        controller.params.id = user.id
        controller.params.login = 'new login'
        controller.params.password = 'new password'
        controller.params.name = 'new name'
        controller.params.surname = 'new surname'

        controller.update()
        user.with {
            assert login == 'login'
            assert password == 'pass'
            assert name == 'new name'
            assert surname == 'new surname'
        }
    }

    void testUpdateValidationError() {
        JSON.registerObjectMarshaller(new ValidationErrorsMarshaller())
        controller.params.id = user.id
        controller.params.name = ''

        controller.update()
        def response = JSON.parse(controller.response.contentAsString)
        response.errors.with {
            assert size() == 1 && first().field == 'name'
        }
    }
}