15.1.3 Unit Testing Domains 3.2.6

(Quick Reference) # 15.1.3 Unit Testing Domains Version: 3.2.6 ## 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:
    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 `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. src/main/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. #### 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
    }
| Quick Reference (hide)