Grails Domain Expectations Plugin

From Lone Cyprus

Contents


About

The Grails Domain Expectations Plugin is a Grails plugin which was written to help make developers' lives easier when writing domain constraints in a Test Driven Development (TDD) fashion.

When writing a test using the Grails Domain Expectations Plugin, a developer expresses their intent by stating an expectation. An expectation is comprised of the name of the property to be affected, the constraint to be applied, and the parameter value(s) which are necessary for the constraint to work.

Goals

The primary goals of this plugin are:

  • Ensure continuity between your domain specification and domain class implementation (unit)
  • Ensure continuity between your domain specification and domain entity implementation (integration)

If you always generate your data model from your domain classes then the first goal will be sufficient. However, if you can't generate your data model from source because you are either working with a legacy database or you are supporting a production Grails application, then the second goal becomes really important.

Needless to say, the second goal is much more difficult to implement than the first. Therefore current development has been focused on getting the first goal finished and polished before tackling the second goal.

Installation

To install the plugin, download the version you want to install from here and type the following on the command line in the location of your Grails project:

grails install-plugin [PATH]/grails-domain-expectations-[VERSION].zip

Getting Started

After installing the Grails Domain Expectations Plugin into your Grails project, the only setup you need to do is to apply the expectations framework to your domain class as follows:

Expectations.applyTo(DomainClass)

Preferably you should place this code in the setUp() method of your GroovyTestCase or GrailsUnitTestCase.

If you want to know what are the possible expectations you can use, simply type the following code in a test method, run the test, and read the standard output:

domain.expectHelp()

Example: Constraint Specification

You're now ready to write your first test! Let's assume that we want to create a new Person class with a first name, we could write our first test this way:

public void testShouldHaveAFirstNameWithAtMost10Characters() {
    def person = new Person()
    person.expectFirstNameIsNotNullable()
    person.expectFirstNameHasMaxSize(10)
}

Equivalently, you can also write the same test this way as well:

public void testShouldHaveAFirstNameWithAtMost10Characters() {
    def person = new Person()
    person."first name is not nullable"
    person."first name has max size" 10
}

After running the test, you should see the following error from expectFirstNameIsNotNullable:

Expected the 'nullable' constraint for the 'Person.firstName' property to be 'false'.

Let's add the following code to Person:

static constraints = {
    firstName(nullable: false)
}

And run the test again to get the following error:

Expected the 'maxSize' constraint for the 'Person.firstName' property to be '10'.

Let's modify the constraint in Person:

static constraints = {
    firstName(nullable: false, maxSize: 10)
}

Run the test again and you should see a green bar!

Alternatively, let's say we set maxSize as '9' instead. Since we have an expectation defined with the value of '10' we should see the following message instead:

Expected the 'maxSize' constraint for the 'Person.firstName' property to be '10', but was '9'.

Furthermore, if firstName was declared with a different type (i.e. Long) then we will see this message instead:

The 'maxSize' constraint expects the 'Person.firstName' property to be of type '[]', 'java.util.Collection',
or 'java.lang.String', but was 'java.lang.Long'.

Example: Constraint Validation

Once you have defined what your constraint is, the next step would be to prove that its working correctly. There are two ways that you can validate a property.

One way is by validating the current state of your domain object:

def person = Person(firstName: 'this is really too long')
person.expectFirstNameIsValid()  // fail!

And the other way is to validate your property by providing a parameter value:

def person = Person(firstName: 'this is really too long')
person.expectFirstNameIsValid('this works')  // pass!
println person.firstName // 'this is really too long'

The above example is pretty trivial, but consider those times that you are using the 'matches' constraint. Wouldn't it be nice to test your regular expression for correctness?

def person = new Person()
person.expectPhoneNumberIsNullable()
person.expectPhoneNumberHasMaxSize(15)
person.expectPhoneNumberMatches(
    '^[01]?\\s*[\\(\\.-]?(\\d{3})[\\)\\.-]?\\s*(\\d{3})[\\.-](\\d{4})$')

person.expectPhoneNumberIsValid("651-555-1212")
person.expectPhoneNumberAreValid "651-555-1212", "612-555-1212"

In the phone number example, if you entered a phone number which does not match the pattern, you will then see the following error message when you run your tests:

The following errors were encountered when validating the Person.phoneNumber' property:
Property [phoneNumber] of class [class Person] with value [555-1212] does not match the required pattern [^[01]?\s*[\(\.-]?(\d{3})[\)\.-]?\s*(\d{3})[\.-](\d{4})$]

To test for a phone number that you expect won't match, you can even do the following:

person."phone number is not valid" "555-1212", "matches"

person.expectPhoneNumberAreNotValid([
    "555-1212" : 'matches',
    "555-1313" : 'matches'
])

Finally, say our Person domain class has an email property with 'minSize' and 'email' constraints declared. You can even perform isNotValid or areNotValid checks and pass in all constraints you expect to fail:

person."email is not valid" "blah", ['email', 'minSize']

person.expectPhoneNumberAreNotValid([
    "blah" : ['email', 'minSize'],
    "long enough, but invalid" : 'email'
])

Example: Run Unit Test as Integration Test

Once you have your unit test written, you might want to run it as an integration test as well. Instead of copying and pasting your tests and keeping both in sync, you can flag a unit test to be run as an integration test as well by adding this to your test class:

def runAsIntegrationTest = true // OR
boolean runAsIntegrationTest = true

Additionally, if you want to turn off mocking when your unit test is being executed as an integration test, you can do the following:

public class PersonTests extends GrailsUnitTestCase {

  boolean runAsIntegrationTest = true  

  public void setUp() {
    super.setUp()
    if (!System.properties['runningAsIntegrationTest']) {
      mockDomain(Person, [])
    }
    Expectations.applyTo(Person)
  }
}

What’s nice about this approach is that can turn off those mocks that you wish to test against the live database and keep all other mocks turned on.

Built-in Constraint Expectations

Here is a list of all the expectations the plugin supports:

Constraint Positive Suffix Negative Suffix
blank IsBlank IsNotBlank
creditCard IsACreditCard IsNotACreditCard
email IsAnEmailAddress IsNotAnEmailAddress
inList IsInList IsNotInList
matches DoesMatch DoesNotMatch
max HasMax DoesNotHaveMax
maxSize HasMaxSize DoesNotHaveMaxSize
min HasMin DoesNotHaveMin
minSize HasMinSize DoesNotHaveMinSize
notEqual NotEqual Equal
nullable IsNullable IsNotNullable
password IsAPassword IsNotAPassword
range IsInRange IsNotInRange
scale HasScale DoesNotHaveScale
unique IsUnique IsNotUnique
url IsAUrl IsNotAUrl
validator HasAValidator DoesNotHaveAValidator
IsValid IsNotValid
AreValid AreNotValid

Downloads

Here are all the different plugin versions that can be downloaded:

Version Notes
0.6.1
  • Added fix to allow BigDecimals to be used in min/max constraints
0.6
  • Added "runAsIntegrationTest" property to allow a unit test to be also executed as an integration test
0.5
  • Added option for the user to not provide a parameter for the 'unique' expectation (i.e. true/false)
  • Display a message to remind the user to run their expectations with the testing plugin or as an integration test
0.4
  • Added the 'validator' expectation
  • Upgraded plugin to Grails 1.1 from Grails 1.0.5-SNAPSHOT
0.3
  • Added the 'areValid' expectation
  • Added the 'isUnique' expectation
  • Added 'expect*Not*' expectations
  • Added optional support for "more human readable expectations"
0.2
  • Added the 'isValid' expectation
0.1
  • Goal 1 - support for all constraints except 'unique' and 'validator'

For those of you that like to live on the edge and get satisfaction from doing your own compiles, you may check out the plugin code from the Mercurial (BitBucket) Repository.

License

Copyright 2009-2011 Lone Cyprus Enterprises, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.