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.9
15.1.3 Unit Testing Domains
Overview
Domain class interaction can be tested without involving a real database connection usingDomainClassUnitTestMixin
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
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 { // … }
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 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 { // … }
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 3 types of validateable classes:- Domain classes
- Classes which implement the
Validateable
trait - Command Objects which have been made validateable automatically
TestFor
or explicitly applies the GrailsUnitTestMixin
using TestMixin
. See the examples below.// src/groovy/com/demo/MyValidateable.groovy package com.democlass 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.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() } }
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 } }
HibernateTestMixin
.dependencies { testCompile 'org.grails:grails-datastore-test-support:4.0.4.RELEASE' }
dependencies {
compile "org.grails.plugins:hibernate:4.3.8.1"
}
Configuring domain classes for HibernateTestMixin tests
Thegrails.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.