Fork me on Github

How to test Domain class constraints?

Authors: Sergio del Amo

Grails Version: 3.3.0

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide, you are going to learn how to define constraints for your Domain classes and test those constraints in isolation.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with additional some code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-test-domain-class-constraints/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-test-domain-class-constraints/complete

3 Writing the Application

3.1 Domain Class

Create a persistent entity to store Hotel entities. Most common way to handle persistence in Grails is the use of Grails Domain Classes:

A domain class fulfills the M in the Model View Controller (MVC) pattern and represents a persistent entity that is mapped onto an underlying database table. In Grails a domain is a class that lives in the grails-app/domain directory.

./grailsw create-domain-class Hotel
CONFIGURE SUCCESSFUL

| Created grails-app/domain/demo/Hotel.groovy
| Created src/test/groovy/demo/HotelSpec.groovy

The Hotel domain class is our data model. We define different properties to store the Hotel characteristics.

/grails-app/domain/demo/Hotel.groovy
package demo

@SuppressWarnings('DuplicateNumberLiteral')
class Hotel {
    String name
    String url
    String email
    String about
    BigDecimal latitude
    BigDecimal longitude

    static constraints = {
        name blank: false, maxSize: 255
        url nullable: true, url: true, maxSize: 255
        email nullable: true, email: true, unique: true
        about nullable: true
        // tag::latitudeCustomValidator[]
        latitude nullable: true, validator: { val, obj, errors ->
            if ( val == null ) {
                return true
            }
            if (val < -90.0) {
                errors.rejectValue('latitude', 'range.toosmall')
                return false
            }
            if (val > 90.0) {
                errors.rejectValue('latitude', 'range.toobig')
                return false
            }
            true
        }
        // end::latitudeCustomValidator[]
        longitude nullable: true, validator: { val, obj, errors ->
            if ( val == null ) {
                return true
            }
            if (val < -180.0) {
                errors.rejectValue('longitude', 'range.toosmall')
                return false
            }
            if (val > 180.0) {
                errors.rejectValue('longitude', 'range.toobig')
                return false
            }
            true
        }
    }

    // tag::hotelMapping[]
    static mapping = {
        about type: 'text'
    }
    // end::hotelMapping[]
}

We define multiple validation constraints

Constraints provide Grails with a declarative DSL for defining validation rules, schema generation and CRUD generation meta data.

Grails comes with multiple constraints ready to use:

You can define your custom validators if needed.

3.2 Unit Tests

We want to unit test our validation constraints.

Please, note the annotation @TestFor. We indicate we are testing a Grails artifact; a domain class.

/src/test/groovy/demo/HotelSpec.groovy
class HotelSpec extends Specification implements DomainUnitTest<Hotel> {

This is the key take-away of this guide:

When you pass a list of Strings to the validate method, the validate method will only validate the properties you pass. A domain class may have many properties. This allows you to tests properties validation on isolation.

Name property constraints testing

We want to map our name property to a relational database with a VARCHAR(255). We want this property to be required and with a max length of 255 characters.

/src/test/groovy/demo/HotelSpec.groovy
void 'test name cannot be null'() {
    when:
    domain.name = null

    then:
    !domain.validate(['name'])
    domain.errors['name'].code == 'nullable'
}

void 'test name cannot be blank'() {
    when:
    domain.name = ''

    then:
    !domain.validate(['name'])
}

void 'test name can have a maximum of 255 characters'() {
    when: 'for a string of 256 characters'
    String str = 'a' * 256
    domain.name = str

    then: 'name validation fails'
    !domain.validate(['name'])
    domain.errors['name'].code == 'maxSize.exceeded'

    when: 'for a string of 256 characters'
    str = 'a' * 255
    domain.name = str

    then: 'name validation passes'
    domain.validate(['name'])
}

Url property constraints testing

We want to map our url property to a relational database with a VARCHAR(255). We want this property to be either a valid url or a null or blank value.

/src/test/groovy/demo/HotelSpec.groovy
@Ignore
void 'test url can have a maximum of 255 characters'() {
    when: 'for a string of 256 characters'
    String urlprefifx = 'http://'
    String urlsufifx = '.com'
    String str = 'a' * (256 - (urlprefifx.size() + urlsufifx.size()))
    str = urlprefifx + str + urlsufifx
    domain.url = str

    then: 'url validation fails'
    !domain.validate(['url'])
    domain.errors['url'].code == 'maxSize.exceeded'

    when: 'for a string of 256 characters'
    str = "${urlprefifx}${'a' * (255 - (urlprefifx.size() + urlsufifx.size()))}${urlsufifx}"
    domain.url = str
    then: 'url validation passes'
    domain.validate(['url'])
}

@Unroll('Hotel.validate() with url: #value should have returned #expected with errorCode: #expectedErrorCode')
void "test url validation"() {
    when:
    domain.url = value

    then:
    expected == domain.validate(['url'])
    domain.errors['url']?.code == expectedErrorCode

    where:
    value                  | expected | expectedErrorCode
    null                   | true     | null
    ''                     | true     | null
    'http://hilton.com'    | true     | null
    'hilton'               | false    | 'url.invalid'
}

Email property constraints testing

We want email to be either a valid email or a null or blank value.

/src/test/groovy/demo/HotelSpec.groovy
@Unroll('Hotel.validate() with email: #value should have returned #expected with errorCode: #expectedErrorCode')
void "test email validation"() {
    when:
    domain.email = value

    then:
    expected == domain.validate(['email'])
    domain.errors['email']?.code == expectedErrorCode

    where:
    value                  | expected | expectedErrorCode
    null                   |  true    | null
    ''                     |  true    | null
    '[email protected]'   |  true    | null
    'hilton'               |  false   | 'email.invalid'
}
Email validator ensures it has less than 255 characters

Email Unique constraint testing

We have added a unique constraint to the Hotel’s email address.

Unique: It constrains a property as unique at the database level. Unique is a persistent call and will query the database.

You can test a unique constraint with such a test:

/src/test/groovy/demo/HotelEmailUniqueConstraintSpec.groovy
package demo

import grails.test.hibernate.HibernateSpec

@SuppressWarnings('MethodName')
class HotelEmailUniqueConstraintSpec extends HibernateSpec {

    List<Class> getDomainClasses() { [Hotel] }

    def "hotel's email unique constraint"() {

        when: 'You instantiate a hotel with name and an email address which has been never used before'
        def hotel = new Hotel(name: 'Hotel Transilvania', email: '[email protected]')

        then: 'hotel is valid instance'
        hotel.validate()

        and: 'we can save it, and we get back a not null GORM Entity'
        hotel.save()

        and: 'there is one additional Hotel'
        Hotel.count() == old(Hotel.count()) + 1

        when: 'instanting a different hotel with the same email address'
        def hilton = new Hotel(name: 'Hilton Hotel', email: '[email protected]')

        then: 'the hotel instance is not valid'
        !hilton.validate(['email'])

        and: 'unique error code is populated'
        hilton.errors['email']?.code == 'unique'

        and: 'trying to save fails too'
        !hilton.save()

        and: 'no hotel has been added'
        Hotel.count() == old(Hotel.count())
    }
}

About property constraints testing

We want about to be a null, blank or a block of text. We don’t want about to be constrained to 255 characters. We used the mapping block in our domain class to indicate this characteristic.

/grails-app/domain/demo/Hotel.groovy
static mapping = {
    about type: 'text'
}

These are the tests to validate about constraints.

/src/test/groovy/demo/HotelSpec.groovy
void 'test about can be null'() {
    when:
    domain.about = null

    then:
    domain.validate(['about'])
}

void 'test about can be blank'() {
    when:
    domain.about = ''

    then:
    domain.validate(['about'])
}

void 'test about can have a more than 255 characters'() {
    when: 'for a string of 256 characters'
    String str = 'a' * 256
    domain.about = str

    then: 'about validation passes'
    domain.validate(['about'])
}

Latitude and longitude properties constraints testing

We want latitude to be null or a value between -90 to 90 degrees. We want longitude to be null or a value between -180 to 180 degrees.

You maybe tempted to use:

latitude nullable: true, range: -90..90
longitude nullable: true, range: -180..180

However, a value such as latitude of 90.1 will be a valid value using the Range Constraint

Set to a Groovy range which can contain numbers in the form of an IntRange, dates or any object that implements Comparable and provides next and previous methods for navigation.

Instead we are using a custom validation.

/grails-app/domain/demo/Hotel.groovy
latitude nullable: true, validator: { val, obj, errors ->
    if ( val == null ) {
        return true
    }
    if (val < -90.0) {
        errors.rejectValue('latitude', 'range.toosmall')
        return false
    }
    if (val > 90.0) {
        errors.rejectValue('latitude', 'range.toobig')
        return false
    }
    true
}

These are the tests to validate latitude and longitude constraints.

/src/test/groovy/demo/HotelSpec.groovy
@Unroll('Hotel.validate() with latitude: #value should have returned #expected with errorCode: #expectedErrorCode')
void 'test latitude validation'() {
    when:
    domain.latitude = value

    then:
    expected == domain.validate(['latitude'])
    domain.errors['latitude']?.code == expectedErrorCode

    where:
    value                  | expected | expectedErrorCode
    null                   | true     | null
    0                      | true     | null
    0.5                    | true     | null
    90                     | true     | null
    90.5                   | false    | 'range.toobig'
    -90                    | true     | null
    -180                   | false    | 'range.toosmall'
    180                    | false    | 'range.toobig'
}

@Unroll('Hotel.longitude() with latitude: #value should have returned #expected with error code: #expectedErrorCode')
void 'test longitude validation'() {
    when:
    domain.longitude = value

    then:
    expected == domain.validate(['longitude'])
    domain.errors['longitude']?.code == expectedErrorCode

    where:
    value                  | expected | expectedErrorCode
    null                   | true     | null
    0                      | true     | null
    90                     | true     | null
    90.1                   | true     | null
    -90                    | true     | null
    -180                   | true     | null
    180                    | true     | null
    180.1                  | false    | 'range.toobig'
    -180.1                 | false    | 'range.toosmall'
}

4 Testing the Application

To run the tests:

./grailsw
grails> test-app
grails> open test-report

or

./gradlew check
open build/reports/tests/index.html