Code for this blog can be found on github at
https://github.com/galapagosfinch

Saturday, May 28, 2011

Constraint tests in Grails

Constraints are an integral part of using Domain classes in Grails. Setting limits on values makes the app stronger by detecting unexpected inputs. (What's that phrase, "expect the unexpected"?) But another integral part of all Java development is insuring that the app is correct via unit tests. Constraints are easy to test, but often overlooked. Consider this, however: how much of your business logic is depending on those constraints?  How much of the code in those constraints are your business logic?  Seems foolish not to test them, now, doesn't it?

Consider this simple class:

package com.springminutes.example

class Cow {

    Breed breed
    String color
    Long legs

    static constraints = {
        color(blank:false, nullable: false)
        legs(nullable: false, validator: { Long l, Cow c ->
            def maxLegs = (c.breed == Breed.Guernsey ? 4 : 10)
            c.legs < 0 || c.legs > maxLegs ? "invalid.range" : null
        })
    }
}

The breed and color properties aren't that interesting, but we should test them nonetheless.  When building constraint tests, Grails gives us a simple way to get the constraints up and running:

class CowTests extends GrailsUnitTestCase {

void testConstraints() {
mockForConstraintsTests(Cow)

mockForConstraintsTests outfits a domain class with all of the validation plumbing needed to test to see if your expectations are correct. What next? Let's start with a positive test:

// Good cow
def cow1 = new Cow(breed: Breed.TasmanianGrey, color: "mottled", legs: 7)
assertTrue cow1.validate()
assertEquals 0, cow1.errors.allErrors.size()

We set all the properties to valid values, assert that validation succeeds and (just to be a little more thorough) assert that there are no errors attached to the object. What next? Negative tests!

// Cow with unknown legs
def cow2 = new Cow(breed: Breed.TexasLonghorn, color: "blue")
assertFalse cow2.validate()
assertEquals "nullable", cow2.errors['legs']

Here we have a Cow without setting the number of legs. We expect validation to fail, and for the "nullable" error to be attached to the legs.

// Cow with blank color
def cow3 = new Cow(breed: Breed.TexasLonghorn, color: "", legs: 1)
assertFalse cow3.validate()
assertEquals "blank", cow3.errors['color']

Here we've set all the properties, but the color is an empty string. We expect validation to fail, and for the "blank" error to be attached to the color.

// Custom validator
// Negative legs
def cow4 = new Cow(breed: Breed.Guernsey, color: "wonky", legs: -1)
assertFalse cow4.validate()
assertEquals "invalid.range", cow4.errors['legs']
// Too many legs for a Guernsey
cow4.legs = 7
assertFalse cow4.validate()
assertEquals "invalid.range", cow4.errors['legs']
cow4.breed = Breed.Holstein
// same cow, after correcting the breed
assertTrue cow4.validate()
assertNull cow4.errors['legs']

Finally, that custom validator. We set up our Cow to be a Guernsey with -1 legs. (I guess he's already paid an arm on his mortgage and only owes a leg, har har). We expect validation to fail and for an "invalid.range" error code to be attached to the legs. Next, we set the number of legs to 7 (Guernseys would *never* be caught dead with more than four legs). Again, we expect validation to fail with the same error message. Finally, we change the breed (Holsteins are not particular) and rerun the validation. The legs property is fine now, and we expect no error attached to it.

Constraint validation is an important part of your model, and you should definitely not skimp on testing that business logic.

So, how much do you test your constraints?

1 comment: