import grails.test.mixin.TestFor
import grails.test.mixin.Mock
import spock.lang.Specification
@TestFor(BookController)
@Mock(Book)
class BookControllerSpec extends Specification {
// ...
}
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:
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.
package com.demo
class MyValidateable implements grails.validation.Validateable {
String name
Integer age
static constraints = {
name matches: /[A-Z].*/
age range: 1..99
}
}
package com.demo
class Person {
String name
static constraints = {
name matches: /[A-Z].*/
}
}
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
}
}
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()
}
}
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'
}
}
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
}
}
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
}