12.1.3 Unit Testing Domains - Reference Documentation
Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith, Lari Hotari
Version: 2.3.8
12.1.3 Unit Testing Domains
Overview
The mocking support described here is best used when testing non-domain artifacts that use domain classes, to let you focus on testing the artifact without needing a database. But when testing persistence it's best to use integration tests which configure Hibernate and use a database.Domain class interaction can be tested without involving a database connection using
DomainClassUnitTestMixin. This implementation mimics the behavior of GORM against an in-memory ConcurrentHashMap implementation. Note that this has limitations compared to a real GORM implementation. The following features of GORM for Hibernate can only be tested within an integration test:
- String-based HQL queries
- composite identifiers
- dirty checking methods
- any direct interaction with Hibernate
DomainClassUnitTestMixin including:
- Simple persistence methods like
save(),delete()etc. - Dynamic Finders
- Named Queries
- Query-by-example
- GORM Events
GrailsUnitTestMixin's mockFor method can come in handy to mock the missing pieces. Alternatively you can write an integration test which bootstraps the complete Grails environment at a cost of test execution time.The 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 spock.lang.Specification@TestFor(BookController) @Mock(Book) class BookControllerSpec extends Specification { // … }
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])
}
}
}import grails.test.mixin.TestFor 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 spock.lang.Specification@TestFor(BookController) @Mock([Book, Author]) class BookControllerSpec extends Specification { // … }
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 } }
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 4 types of validateable classes:- Domain classes
- Classes marked with the
Validateableannotation - Command Objects which have been made validateable automatically
- Classes configured to be validateable via the
grails.validateable.classesproperty inConfig.groovy
TestFor or explicitly applies the GrailsUnitTestMixin using TestMixin. See the examples below.// src/groovy/com/demo/MyValidateable.groovy package com.demo@grails.validation.Validateable class MyValidateable { String name Integer age static constraints = { name matches: /[A-Z].*/ age range: 1..99 } }
// grails-app/domain/com/demo/Person.groovy package com.democlass Person { String name static constraints = { name matches: /[A-Z].*/ } }
// grails-app/controllers/com/demo/DemoController.groovy package com.democlass 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.demoimport 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.demoimport 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.demoimport 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.demoimport 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() } }
grails.validateable.classes property in Config.groovy, one additional step is required to test validation. GrailsUnitTestMixin provides a method named mockForConstraintsTests that will mock validation support for these classes. See the example below.// src/groovy/com/demo/Book.groovy package com.democlass Book { String title String author static constraints = { author minSize: 5 } }
// grails-app/conf/Config.groovy grails.validateable.classes = [com.demo.Book]// ...
// test/unit/com/demo/BookSpec.groovy package com.demoimport grails.test.mixin.TestMixin import grails.test.mixin.support.GrailsUnitTestMixin import spock.lang.Specification@TestMixin(GrailsUnitTestMixin) class BookSpec extends Specification { void 'Test validation'() { given: mockForConstraintsTests Book when: 'the author name has only 4 characters' def book = new Book() book.author = 'Jeff' then: 'validation should fail' !book.validate() book.hasErrors() book.errors['author'] == 'minSize' when: 'the author name has 5 characters' book.author = 'Jacob' then: 'validation should pass' book.validate() !book.hasErrors() } }
mockForConstraintsTests method changes the behavior of the errors object such that something like book.errors'author' will evaluate to the name of the failed constraint, not a org.springframework.validation.FieldError object. This is convenient for unit tests. If your unit test really does want a reference to the org.springframework.validation.FieldError object use something like book.errors.getFieldError('author').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.

