Show Navigation

Grails Service Testing

In this guide we are going to explore Service testing in Grails. Unit test GORM, Integration Tests, Mocking collaborators...

Authors: Nirav Assar

Grails Version: 4.0.1

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide you are going explore Service Testing in Grails. You are going to learn about:

  • Grails Services Unit tests - Mocking Collaborators

  • Grails Services Unit tests - GORM code

  • Grails Services Integration tests

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-mock-basics/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-mock-basics/complete

3 Writing the Application

We are going to write a simple application involving a Classroom and a Student domain class.

Several services will interact with those domain classes. We will use those services to discuss several testing topics.

3.1 Domain Classes

We will create Domain classes that serve as the basis for the application. The Student and Classroom have a one-to-many relationship, where the Classroom holds many Student(s). A Student can be enrolled in one Classroom or none.

> grails create-domain-class Student

Add domain properties to the created class file.

grails-app/domain/grails/mock/basics/Student.groovy
package grails.mock.basics

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Student {
    String name
    BigDecimal grade
    Classroom classroom

    static constraints = {
        classroom nullable: true
    }
}
> grails create-domain-class Classroom

Again, add domain properties to the created class file. This time note that Classroom hasMany Students. This creates the one-to-many relationship.

grails-app/domain/grails/mock/basics/Classroom.groovy
package grails.mock.basics

import groovy.transform.CompileStatic

@CompileStatic
class Classroom {

    String teacher
    static hasMany = [students: Student]
}

3.2 Gorm Hibernate

We are using the GORM 7.0.2.RELEASE.

gradle.properties
grailsVersion=4.0.1
gormVersion=7.0.2.RELEASE
build.gradle
buildscript {
...
    dependencies {
        ...
        classpath "org.grails.plugins:hibernate5:7.0.0"  (1)
    }
}

...
dependencies {
...
    compile "org.grails.plugins:hibernate5" (1)
    compile "org.hibernate:hibernate-core:5.4.0.Final"  (1)
    }
}
1 For this guide we are using GORM Hibernate implementation

3.3 DynamicFinder Service

The next service uses a GORM Dynamic Finder to get a list of students with a grade above a threshold.

grails-app/services/grails/mock/basics/StudentService.groovy
package grails.mock.basics

import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional


@GrailsCompileStatic
@Transactional(readOnly = true)
class StudentService {

    List<Student> findStudentsWithGradeAbove(BigDecimal grade) {
        Student.findAllByGradeGreaterThanEquals(grade)
    }
}

3.4 HibernateSpec

We are going to use a unit test for the dynamic finder with the help of HibernateSpec. It allows Hibernate to be used in Grails unit tests. It uses a H2 in-memory database.

src/test/groovy/grails/mock/basics/StudentServiceSpec.groovy
package grails.mock.basics

import grails.test.hibernate.HibernateSpec
import grails.testing.services.ServiceUnitTest

@SuppressWarnings(['MethodName', 'DuplicateNumberLiteral'])
class StudentServiceSpec extends HibernateSpec implements ServiceUnitTest<StudentService> {

    List<Class> getDomainClasses() { [Student] } (1)
    def 'test find students with grades above'() {
        when: 'students are already stored in db'
        Student.saveAll(
            new Student(name: 'Nirav', grade: 91),
            new Student(name: 'Sergio', grade: 95),
            new Student(name: 'Jeff', grade: 93),
        )

        then:
        Student.count() == 3

        when: 'service is called to search'
        List<Student> students = service.findStudentsWithGradeAbove(92)

        then: 'students are found with appropriate grades'
        students.size() == 2
        students[0].name == 'Sergio'
        students[0].grade == 95
        students[1].name == 'Jeff'
        students[1].grade == 93
    }
}
1 Test a limited set of domain classes, override getDomainClasses method and specify exactly which ones you wish to test.
2 If the service under test uses @Transactional you will need to assign the transaction manager within the unit test’s setup method.

3.5 Projection Query

The next service uses Criteria query with a projection to calculate the average grade of the students enrolled in a classroom, identified by its teacher name.

grails-app/services/grails/mock/basics/ClassroomService.groovy
package grails.mock.basics

import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Transactional(readOnly = true)
class ClassroomService {

    BigDecimal calculateAvgGrade(String teacherName) {
        Student.where {
            classroom.teacher == teacherName
        }.projections {
            avg('grade')
        }.get() as BigDecimal
    }
}

3.6 Service Integration tests

We could unit test a project query with a unit test. See:

.src/test/groovy/grails/mock/basics/ClassroomServiceSpec.groovy

However, you may want to use an integration test. For example, you may want to test the query against the same database which you use in production.

Just use @Autowired to inject your service into your integration tests as displayed below:

src/integration-test/groovy/grails/mock/basics/ClassroomServiceIntegrationSpec.groovy
package grails.mock.basics

import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration

import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification

@SuppressWarnings('MethodName')
@Rollback
@Integration
class ClassroomServiceIntegrationSpec extends Specification {

    @Autowired ClassroomService service

    void 'test calculate average grade of classroom'() {
        when:
        def classroom = new Classroom(teacher: 'Smith')
        [
                [name: 'Nirav', grade: 91],
                [name: 'Sergio', grade: 95],
                [name: 'Jeff', grade: 93],
        ].each {
            classroom.addToStudents(new Student(name: it.name, grade: it.grade))
        }
        classroom.save()

        then:
        Classroom.count() == 1
        Student.count() == 3

        when:
        BigDecimal avgGrade = service.calculateAvgGrade('Smith')

        then:
        avgGrade == 93.0
    }
}

3.7 Service with Collaborators

We have a service which collaborates with another service to perform an action. This is a common use case. We are going to learn how to mock collaborators to test the service logic in isolation.

grails-app/services/grails/mock/basics/ClassroomGradesNotificationService.groovy
package grails.mock.basics

import groovy.transform.CompileStatic

@CompileStatic
class ClassroomGradesNotificationService {

    EmailService emailService

    int emailClassroomStudents(Classroom classroom) {
        int emailCount = 0
        for ( Student student in classroom.students ) {
            def email = emailFromTeacherToStudent(classroom.teacher, student)
            emailCount += emailService.sendEmail(email)
        }
        emailCount
    }

    Map emailFromTeacherToStudent(String teacher, Student student) {
        [
                to: "${student.name}",
                from: "${teacher}",
                note: "Grade is ${student.grade}",
        ]
    }
}
grails-app/services/grails/mock/basics/EmailService.groovy
package grails.mock.basics

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class EmailService {

    int sendEmail(Map message) {
        log.info "Send email: ${message}"
        1
    }
}

3.8 Test Email with Mock Collaborators

The Spock framework gives the ability to mock collaborators. We are able to substitute dependencies within a service with a mock implementation. This gives the advantage of focusing on the functionality under test, while delegating the dependent functionality to a mock object. In addition, mock collaborators can be verified to see how many times they are called and which parameters are being sent.

Implement the code to test email with a mock collaborator. Essentially, we want to verify the email service is called for each Student in a Classroom.

src/test/groovy/grails/mock/basics/ClassroomGradesNotificationServiceSpec.groovy
package grails.mock.basics


import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Shared
import spock.lang.Specification


@SuppressWarnings('MethodName')
class ClassroomGradesNotificationServiceSpec extends Specification
        implements DataTest, ServiceUnitTest<ClassroomGradesNotificationService> {

    @Shared Classroom classroom

    def setupSpec() { (1)
        mockDomain Student
        mockDomain Classroom
    }

    def setup() {
        classroom = new Classroom(teacher: 'Smith')
        [
                [name: 'Nirav', grade: 91],
                [name: 'Sergio', grade: 95],
                [name: 'Jeff', grade: 93],
        ].each {
            classroom.addToStudents(new Student(name: it.name, grade: it.grade))
        }
    }

    void 'test email students with mock collaborator'() {
        given: 'students are part of a classroom'
        def mockService = Mock(EmailService) (2)
        service.emailService = mockService (3)

        when: 'service is called to email students'
        int emailCount = service.emailClassroomStudents(classroom) (4)

        then:
        1 * mockService.sendEmail([to: 'Sergio', from: 'Smith', note: 'Grade is 95']) >> 1 (5)
        1 * mockService.sendEmail([to: 'Nirav', from: 'Smith', note: 'Grade is 91']) >> 1
        1 * mockService.sendEmail([to: 'Jeff', from: 'Smith', note: 'Grade is 93']) >> 1
        emailCount == 3
    }

}
1 Mock domain classes
2 Instantiate the mock implementation of EmailService.
3 Inject the mock into the service.
4 Invoke the service under test, which now has a mock implementation of EmailService.
5 Use the spock syntax to verify invocation, params, and return a value.

In the then: clause, we use spock to verify the mock was called. With 1 *, this verfies it was called once. The parameters defined verify what was passed in during execution. The >> 1 give the mock stub capability where it will return 1.

Essentially, we are verifying the mock was called three different times with different Student parameters.

4 Summary

To summarize this guide, we learned how . . .

  • To unit test Service’s methods with GORM code, use HibernateSpec.

  • To create an integration tests for a service, inject the service with @Autowired.

  • Mocking Services can be done with the Spock Mock() method. It abides by lenient mock principles, and comes with an array of features.

5 Running the Application

To run the tests:

./grailsw
grails> test-app
grails> open test-report

or

./gradlew test

To run the integration tests

./gradlew integrationTest