(Quick Reference)

15.1.3 Unit Testing Domains - Reference Documentation

Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith, Lari Hotari

Version: 3.1.10

15.1.3 Unit Testing Domains

Overview

Domain class interaction can be tested without involving a real database connection using DomainClassUnitTestMixin or by using the HibernateTestMixin.

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

HibernateTestMixin uses Hibernate 4 and a H2 in-memory database. This makes it possible to use all GORM features also in Grails unit tests.

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

  • String-based HQL queries
  • composite identifiers
  • dirty checking methods
  • any direct interaction with Hibernate

The implementation behind HibernateTestMixin takes care of setting up the Hibernate with the in-memory H2 database. It only configures the given domain classes for use in a unit test. The @Domain annotation is used to tell which domain classes should be configured.

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 SimpleController class and mocks the behavior of the Simple 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 Map 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:

  1. Domain classes
  2. Classes which implement the Validateable trait
  3. 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/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.

HibernateTestMixin Basics

HibernateTestMixin allows Hibernate 4 to be used in Grails unit tests. It uses a H2 in-memory database.

import grails.test.mixin.TestMixin
import grails.test.mixin.gorm.Domain
import grails.test.mixin.hibernate.HibernateTestMixin
import spock.lang.Specification

@Domain(Person) @TestMixin(HibernateTestMixin) class PersonSpec extends Specification {

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

This library dependency is required in build.gradle for adding support for HibernateTestMixin.

dependencies {
        testCompile 'org.grails:grails-datastore-test-support:4.0.4.RELEASE'
    }

HibernateTestMixin is only supported with hibernate4 plugin versions >= 4.3.8.1 .

dependencies {
        compile "org.grails.plugins:hibernate:4.3.8.1"
    }

Configuring domain classes for HibernateTestMixin tests

The grails.test.mixin.gorm.Domain annotation is used to configure the list of domain classes to configure for Hibernate sessionFactory instance that gets configured when the unit test runtime is initialized.

Domain annotations will be collected from several locations:

  • the annotations on the test class
  • the package annotations in the package-info.java/package-info.groovy file in the package of the test class
  • each super class of the test class and their respective package annotations
  • the possible SharedRuntime class

Domain annotations can be shared by adding them as package annotations to package-info.java/package-info.groovy files or by adding them to a SharedRuntime class which has been added for the test.

It's not possible to use DomainClassUnitTestMixin's Mock annotation in HibernateTestMixin tests. Use the Domain annotation in the place of Mock in HibernateTestMixin tests.