(Quick Reference)

15.1.3 Unit Testing Domains

Version: 3.2.8

15.1.3 Unit Testing Domains

Overview

Domain class interaction can be tested without involving a real database connection using DomainClassUnitTestMixin.

The GORM implementation in DomainClassUnitTestMixin is using a simple in-memory ConcurrentHashMap implementation. Note that this has limitations compared to a real GORM implementation.

A large, commonly-used portion of the GORM API can be mocked using DomainClassUnitTestMixin including:

  • Simple persistence methods like save(), delete() etc.

  • Dynamic Finders

  • Named Queries

  • Query-by-example

  • GORM Events

If you wish to test GORM using a real database then it is recommended to use one of the following super classes:

  • grails.test.hibernate.HibernateSpec - Sets up Hibernate for the current test

  • grails.test.mongodb.MongoSpec - Sets up MongoDB for the current test

All features of GORM for Hibernate can be tested within a HibernateSpec unit test including:

  • String-based HQL queries

  • composite identifiers

  • dirty checking methods

  • any direct interaction with Hibernate

The implementation behind HibernateSpec takes care of setting up the Hibernate with the in-memory H2 database.

DomainClassUnitTestMixin Basics

DomainClassUnitTestMixin is typically used in combination with testing either a controller, service or tag library where the domain is a mock collaborator defined by the Mock annotation:

import grails.test.mixin.TestFor
import grails.test.mixin.Mock
import spock.lang.Specification

@TestFor(BookController)
@Mock(Book)
class BookControllerSpec extends Specification {
    // ...
}

The example above tests the BookController class and mocks the behavior of the Book domain class as well. For example consider a typical scaffolded save controller action:

class BookController {
    def save() {
        def book = new Book(params)
        if (book.save(flush: true)) {
            flash.message = message(
                    code: 'default.created.message',
                    args: [message(code: 'book.label', default: 'Book'), book.id])
            redirect(action: "show", id: book.id)
        }
        else {
            render(view: "create", model: [bookInstance: book])
        }
    }
}

Tests for this action can be written as follows:

import grails.test.mixin.TestFor
import grails.test.mixin.Mock
import spock.lang.Specification

@TestFor(BookController)
@Mock(Book)
class BookControllerSpec extends Specification {
   void "test saving an invalid book"() {
        when:
        controller.save()

        then:
        model.bookInstance != null
        view == '/book/create'
    }

    void "test saving a valid book"() {
        when:
        params.title = "The Stand"
        params.pages = "500"

        controller.save()

        then:
        response.redirectedUrl == '/book/show/1'
        flash.message != null
        Book.count() == 1
    }
}

Mock annotation also supports a list of mock collaborators if you have more than one domain to mock:

import grails.test.mixin.TestFor
import grails.test.mixin.Mock
import spock.lang.Specification

@TestFor(BookController)
@Mock([Book, Author])
class BookControllerSpec extends Specification {
    // ...
}

Alternatively you can also use the DomainClassUnitTestMixin directly with the TestMixin annotation and then call the mockDomain method to mock domains during your test:

import grails.test.mixin.TestFor
import grails.test.mixin.TestMixin
import spock.lang.Specification
import grails.test.mixin.domain.DomainClassUnitTestMixin

@TestFor(BookController)
@TestMixin(DomainClassUnitTestMixin)
class BookControllerSpec extends Specification {

    void setupSpec() {
         mockDomain(Book)
    }

    void "test saving an invalid book"() {
        when:
        controller.save()

        then:
        model.bookInstance != null
        view == '/book/create'
    }

    void "test saving a valid book"() {
        when:
        params.title = "The Stand"
        params.pages = "500"

        controller.save()

        then:
        response.redirectedUrl == '/book/show/1'
        flash.message != null
        Book.count() == 1
    }
}

The mockDomain method also includes an additional parameter that lets you pass a List of Maps to configure a domain, which is useful for fixture-like data:

mockDomain(Book, [
            [title: "The Stand", pages: 1000],
            [title: "The Shining", pages: 400],
            [title: "Along Came a Spider", pages: 300] ])

Testing Constraints

There are 3 types of validateable classes:

  • Domain classes

  • Classes which implement the Validateable trait

  • Command Objects which have been made validateable automatically

These are all easily testable in a unit test with no special configuration necessary as long as the test method is marked with TestFor or explicitly applies the GrailsUnitTestMixin using TestMixin. See the examples below.

src/main/groovy/com/demo/MyValidateable.groovy
package com.demo

class MyValidateable implements grails.validation.Validateable {
    String name
    Integer age

    static constraints = {
        name matches: /[A-Z].*/
        age range: 1..99
    }
}
grails-app/domain/com/demo/Person.groovy
package com.demo

class Person {
    String name

    static constraints = {
        name matches: /[A-Z].*/
    }
}
grails-app/controllers/com/demo/DemoController.groovy
package com.demo

class DemoController {

    def addItems(MyCommandObject co) {
        if(co.hasErrors()) {
            render 'something went wrong'
        } else {
            render 'items have been added'
        }
    }
}

class MyCommandObject {
    Integer numberOfItems

    static constraints = {
        numberOfItems range: 1..10
    }
}
test/unit/com/demo/PersonSpec.groovy
package com.demo

import grails.test.mixin.TestFor
import spock.lang.Specification

@TestFor(Person)
class PersonSpec extends Specification {

    void "Test that name must begin with an upper case letter"() {
        when: 'the name begins with a lower letter'
        def p = new Person(name: 'jeff')

        then: 'validation should fail'
        !p.validate()

        when: 'the name begins with an upper case letter'
        p = new Person(name: 'Jeff')

        then: 'validation should pass'
        p.validate()
    }
}
test/unit/com/demo/DemoControllerSpec.groovy
package com.demo

import grails.test.mixin.TestFor
import spock.lang.Specification

@TestFor(DemoController)
class DemoControllerSpec extends Specification {

    void 'Test an invalid number of items'() {
        when:
        params.numberOfItems = 42
        controller.addItems()

        then:
        response.text == 'something went wrong'
    }

    void 'Test a valid number of items'() {
        when:
        params.numberOfItems = 8
        controller.addItems()

        then:
        response.text == 'items have been added'
    }
}
test/unit/com/demo/MyValidateableSpec.groovy
package com.demo

import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification


@TestMixin(GrailsUnitTestMixin)
class MyValidateableSpec extends Specification {

    void 'Test validate can be invoked in a unit test with no special configuration'() {
        when: 'an object is valid'
        def validateable = new MyValidateable(name: 'Kirk', age: 47)

        then: 'validate() returns true and there are no errors'
        validateable.validate()
        !validateable.hasErrors()
        validateable.errors.errorCount == 0

        when: 'an object is invalid'
        validateable.name = 'kirk'

        then: 'validate() returns false and the appropriate error is created'
        !validateable.validate()
        validateable.hasErrors()
        validateable.errors.errorCount == 1
        validateable.errors['name'].code == 'matches.invalid'

        when: 'the clearErrors() is called'
        validateable.clearErrors()

        then: 'the errors are gone'
        !validateable.hasErrors()
        validateable.errors.errorCount == 0

        when: 'the object is put back in a valid state'
        validateable.name = 'Kirk'

        then: 'validate() returns true and there are no errors'
        validateable.validate()
        !validateable.hasErrors()
        validateable.errors.errorCount == 0
    }
}
test/unit/com/demo/MyCommandObjectSpec.groovy
package com.demo

import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification

@TestMixin(GrailsUnitTestMixin)
class MyCommandObjectSpec extends Specification {

    void 'Test that numberOfItems must be between 1 and 10'() {
        when: 'numberOfItems is less than 1'
        def co = new MyCommandObject()
        co.numberOfItems = 0

        then: 'validation fails'
        !co.validate()
        co.hasErrors()
        co.errors['numberOfItems'].code == 'range.toosmall'

        when: 'numberOfItems is greater than 10'
        co.numberOfItems = 11

        then: 'validation fails'
        !co.validate()
        co.hasErrors()
        co.errors['numberOfItems'].code == 'range.toobig'

        when: 'numberOfItems is greater than 1'
        co.numberOfItems = 1

        then: 'validation succeeds'
        co.validate()
        !co.hasErrors()

        when: 'numberOfItems is greater than 10'
        co.numberOfItems = 10

        then: 'validation succeeds'
        co.validate()
        !co.hasErrors()
    }
}

That’s it for testing constraints. One final thing we would like to say is that testing the constraints in this way catches a common error: typos in the "constraints" property name which is a mistake that is easy to make and equally easy to overlook. A unit test for your constraints will highlight the problem straight away.

HibernateSpec and MongoSpec Basics

HibernateSpec allows Hibernate to be used in Grails unit tests. It uses a H2 in-memory database. The following documentation covers HibernateSpec, but the same concepts can be applied to MongoSpec.

package example

import grails.test.hibernate.HibernateSpec
import spock.lang.Specification


class PersonSpec extends HibernateSpec {

    void "Test count people"() {
        expect: "Test execute Hibernate count query"
            Person.count() == 0
            sessionFactory != null
            transactionManager != null
            hibernateSession != null
    }
}

By default HibernateSpec will scan for domain classes within the package specified by the grails.codegen.defaultPackage configuration option or if not specified the same package as the test.

Configuring domain classes for HibernateSpec tests

If you only wish to test a limited set of domain classes you can override the getDomainClasses method to specify exactly which ones you wish to test:

package example

import grails.test.hibernate.HibernateSpec

class PersonSpec extends HibernateSpec {
    ...

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

Testing Controllers and Services with HibernateSpec

If you wish to test a controller or a service with HibernateSpec generally you can combine HibernateSpec with Grails' default test mixins:

package example

import grails.test.hibernate.HibernateSpec

@TestFor(BookController)
class BookControllerUnitSpec extends HibernateSpec {

    ...
}

However, if the controller or service uses @Transactional you will need to assign the transaction manager within the unit test’s setup method:

def setup() {
    controller.transactionManager = transactionManager
}