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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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:
1 |
JSON.registerObjectMarshaller(new ValidationErrorsMarshaller()) |
After the aforementioned changes our passing test class should look like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
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' } } } |
Hi Marcin,
That is a very cool tip! And if you replace @Category(ControllerUnitTestCase) with @Category(ControllerSpec) you can use in your Spock tests.
Best regards,
Søren Berg Glasius
Glad to read that you find the blog post useful!