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.0
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:
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 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/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.