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: 3.3.0
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.7 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-mock-basics.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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.
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.
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 6.1.6.RELEASE.
grailsVersion=3.3.0
grailsWrapperVersion=1.0.0
gormVersion=6.1.6.RELEASE
gradleWrapperVersion=3.5
buildscript {
repositories {
mavenLocal()
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
classpath "org.grails:grails-gradle-plugin:$grailsVersion"
classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.2"
classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}" (1)
}
}
version "0.1"
group "grails.mock.basics"
apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"asset-pipeline"
apply plugin:"org.grails.grails-gsp"
apply plugin: 'codenarc'
repositories {
mavenLocal()
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
compile "org.springframework.boot:spring-boot-starter-logging"
compile "org.springframework.boot:spring-boot-autoconfigure"
compile "org.grails:grails-core"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-tomcat"
compile "org.grails:grails-web-boot"
compile "org.grails:grails-logging"
compile "org.grails:grails-plugin-rest"
compile "org.grails:grails-plugin-databinding"
compile "org.grails:grails-plugin-i18n"
compile "org.grails:grails-plugin-services"
compile "org.grails:grails-plugin-url-mappings"
compile "org.grails:grails-plugin-interceptors"
compile "org.grails.plugins:cache"
compile "org.grails.plugins:async"
compile "org.grails.plugins:scaffolding"
compile "org.grails.plugins:events"
compile "org.grails.plugins:hibernate5" (1)
compile "org.hibernate:hibernate-core:5.1.5.Final"
compile "org.grails.plugins:gsp"
console "org.grails:grails-console"
profile "org.grails.profiles:web"
runtime "org.glassfish.web:el-impl:2.1.2-b03"
runtime "com.h2database:h2"
runtime "org.apache.tomcat:tomcat-jdbc"
runtime "com.bertramlabs.plugins:asset-pipeline-grails:2.14.2"
testCompile "org.grails:grails-gorm-testing-support"
testCompile "org.grails.plugins:geb"
testCompile "org.grails:grails-web-testing-support"
testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
}
bootRun {
jvmArgs('-Dspring.output.ansi.enabled=always')
addResources = true
}
assets {
minifyJs = true
minifyCss = true
}
codenarc {
toolVersion = '0.27.0'
configFile = file("${project.projectDir}/config/codenarc/rules.groovy")
reportFormat = 'html'
}
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.
package grails.mock.basics
import grails.compiler.GrailsCompileStatic
import grails.transaction.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.
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.
package grails.mock.basics
import grails.transaction.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:
package grails.mock.basics
import grails.testing.mixin.integration.Integration
import grails.transaction.Rollback
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.
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}",
]
}
}
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
.
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