Tuesday 20 November 2012

Grails: Unit testing of domain classes

To unit test a domain class with Grails 2.x, we can use the TestFor annotation. Within a test method, a new instance of our domain class will be available as 'domain'. We can set properties on the domain instance and then call validate(). The errors property can be used to check the expected validation errors (if any):

class Book {
    String title

    static constraints = { title minSize: 10 }
}

@TestFor(Book)
class BookTests {
    void testShortTitle() {
        domain.title = 'too short'
        assert !domain.validate()
        assert 'minSize.notmet' == domain.errors.getFieldError('title').code
    }
}

It would be nice if the last assert would allow the shorthand syntax to lookup the title field error code (see also GRAILS-8415):

assert 'minSize' == domain.errors.title

However, that will not work:

--Output from testShortTitle--
| Failure:  testShortTitle(bookstore.BookTests)
|  groovy.lang.MissingPropertyException: No such property: title for class: org.grails.datastore.mapping.validation.ValidationErrors

Why? The errors property is of type org.grails.datastore.mapping.validation.ValidationErrors (or grails.validation.ValidationErrors). This type does support the subscript operator though, so it can be slightly shorter:

assert 'minSize.notmet' == domain.errors['title'].code

But it still needs to read the code property of FieldError, and this code does not match the constraint name.

If we look a bit further at other subclasses of org.springframework.validation.BeanPropertyBindingResult, we see that the shorthand syntax is available in org.codehaus.groovy.grails.plugins.testing.GrailsMockErrors, and it translates the codes to constraint names, so 'minSize' can be used instead of 'minSize.notmet'.

To use GrailsMockErrors, we can call mockForConstraintsTests from our test class. For example, we can call it once for the domain class to mock all instances like this:

@TestFor(Book)
class BookTests {
    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }
    void testShortTitle() {
        domain.title = 'too short'
        assert !domain.validate()
        assert 'minSize' == domain.errors.title
    }
}

The mocking will prepare the Book class, including the errors field, and make the shorthand syntax available.

It's also possible to instantiate our own Book instances instead of using "domain", and test them in the same way. The advantage is that we can use the property map constructor, although it also helps to use the 'with' method on the domain field:

@TestFor(Book)
class BookTests {
    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }
    void testShortTitle() {
        domain.with {
            title = 'too short'
            assert !validate()
            assert 'minSize' == errors.title
        }
    }
}