Grails Testing: Domain Constraints

Lately I’ve been talking to developers on their philosophy to unit testing the code they write. These conversations start of in high level generalities and then quickly get to their testing practices in the Grails space (since that is where I spend most of my time these days). However, to much of my chagrin these conversations have usually ended with the line – “I see the value that unit testing brings, but…”

The “buts” range anywhere from “I don’t have time” to “Grails starts up so fast that I just test everything through the UI” to ” to “All the dynamic stuff makes it hard for me to figure out how to write the test”.

Admittally the documentation around grails testing is a little light and some of the meta-programming mojo going on behid the covers can be a litte intimidating or confusing for those new to the framework, but falling back on these excuses will enevitably leave you and your app in a bad place.

Now let’s see if we can do someting about that.

First off: Testing Constraints in your Domain Class

Initally I thought of constraint testing as “too simple to fail”, like testing getters or setters in Java.
Plus back in the day (pre 1.0) this was a gigantic pain to even think about doing it properly. Now it’s so easy that you might as well do it.

A little background on Domain Constraints

Setting constraints in your domain class do 3 things for you.

1. Some influence how your database is constructed:

+ inList
+ max
+ min
+ maxSize
+ minSize
+ nullable
+ range
+ scale
+ size
+ unique

2. Some provide a “test before save” check:

+ blank
+ creditCard
+ email
+ matches
+ url
+ validator

3. All give you an way to order your fields when used with scaffolding.

So lets say we have this Person Domain Object:

class Person{
	String firstName 
	String lastName
	String userName
	String password
	Address address
	static constraints = {
		firstName(nullable:false, blank:false, max:50, min:2)
		lastName(nullable:false, blank:false)
		userName(nullable:false, blank:false, unique:true)
		password(nullable:false, blank:false, min:8, max:20, matchs:/[a_zA-Z1-0]+/)

Here is how we would test the constraints:

class PersonConstraintsTest extends GrailsUnitTestCase{ //1
	def person
	void setUp(){
		super.setup() //2
		mockForConstraintsTests(Person) //3
		person = new Person(firstName:"Jon", 
				password:"weakpassword")  //4
	void testFirstNameNullable_Pass(){
		assertTrue 'validation shoud have passed ' , person.validate() //5
	void testFirstNameNullable_Fail(){
		person.firstName = null
		assertFalse 'validation shoud have faild' , person.validate() //5
		assertEqual 'should have a nullable error', 'nullable', person.errors["firstName"]//6
	void tearDown(){
		super.tearDown() //7

1. Extend GrailsUnitTestCase: GrailsUnitTestCase extends GroovyTestCase
2. Call the parent classes setUp() method. IMPORTANT: You must do this to get all the testing goodness to work.
3. Call the mockForConstraintsTests method passing in the our domain class. NOTE: This is the class not an instanciatied object.
4. Set the happy path for the domain object. This instance will pass all constraints.
5. Call the ‘validate()’ method on the domain and get a Boolean result.
6. Here we look at the Domain objects error map and see if it has a ‘nullable’ error on the ‘firstName’ field value.
7. Call super.tearDown(): this will roll back the metaClass of the domain object to it’s unmodified state. Probably a good thing to do.

You would then follow this pattern for the rest of the constraints.

So that’s how you test your constraints. Super easy.

Behind the covers

Now here is what happings begind the covers:

By extending the GrailsUnitTestCase and calling the super.setUp() method we get a map to hold our errors.

Here is what Grails does when we call the mockForConstraintsTests method and pass it our Domain class:

1. check the class to make sure it is a Domain Class
2. instanciates a new DefaultGrailsDomainClass
3. adds the “validate” method to the domain object.
4. builds up the constraint list from all constraints in the domain classes hierarchy.
5. builds up the constraint list for all the properties constraints in the domain class hierarchy. This gives up us cascading validation in our tests. Very cool.
6. adds data binding to the domain class constructor
7. finally adds all of the error related methods (ie. getErrors, hasErrors, setErrors) on the domain.

All the metaClass mojo is taken care of for you and all you have to write is 3 little lines:

1. super.setUp()
2. mockForConstraintsTests([Domain])
3. super.tearDown()

P.S. All of this works just the same for testing the constraints on your Command Objects.

If you want to further dig into all of this testing goodness; download the latest version of Grails (anything after 1.1) souces and look at these classes:
– GrailsUnitTestCase
– Mockutils

One thought on “Grails Testing: Domain Constraints

  1. Ross Niemi says:

    Nice post Zan.

    I went down this same path when I started working with Grails several years ago. I felt better for using a similar testing approach, but I found that (re-)reading the unit tests when I revisited them was a much slower activity then reading actual prose (i.e. ‘first name is nullable’). Since the tests were harder for me to read, it was also more difficult for me to refactor both my tests and application when the need came about.

    In order to eliminate/reduce the testing “boiler plate” and to make reading tests easier (i.e. something a non developer can read), I ended up creating the “Grails Domain Expectations Plugin”:

    Oddly enough, both the examples you and I use involve a “Person” class :)

    Please take a look and send me your thoughts. Since you have thought about constraints a fair bit, I’m really looking forward to any feedback that you can provide.

  2. CM says:

    Interesting article Zan!

    The OPI’s technical blog is pretty interesting, I love it. Say, is there a dumb down application you can share with us ?

    Thanks and looking forward for more Grails articles.

Leave a Reply

Your email address will not be published. Required fields are marked *