Grails 服务测试
在本指南中,我们将探索 Grails 中的服务测试。单元测试 GORM、集成测试、模拟协作者...
作者:Nirav Assar
Grails 版本 3.3.0
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和交付!.
2 开始
在本指南中,您将探索 Grails 中的服务测试。您将了解
-
Grails 服务单元测试 - 模拟协作者
-
Grails 服务单元测试 - GORM 代码
-
Grails 服务集成测试
2.1 您需要执行以下操作
若要完成本指南,您将需要以下内容
-
一些时间
-
一个像样的文本编辑器或 IDE
-
安装了 JDK 1.7 或更高版本,并正确配置了 JAVA_HOME
2.2 如何完成本指南
若要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆Git存储库
git clone https://github.com/grails-guides/grails-mock-basics.git
Grails 指南库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,它带有一些其他代码,可让您抢先一步。 -
complete
一个完整的示例。它通过对初始文件夹执行指南中提供的步骤并应用更改的结果。
若要完成本指南,请转到 initial
文件夹
-
cd
进入grails-guides/grails-mock-basics/initial
然后按照下一部分中的说明操作。
cd 进入 grails-guides/grails-mock-basics/complete ,即可直接浏览完整的示例 |
3 编写应用程序
我们准备编写一个简单的应用程序,其中涉及一个 Classroom
(教室)和一个 Student
(学生)领域类。
其他一些服务会与这些领域类进行交互。我们将使用这些服务来讨论多个测试主题。
3.1 领域类
我们将创建领域类,作为应用程序的基础。Student
(学生)和 Classroom
(教室)之间存在一对多关系,其中 Classroom
(教室)拥有很多个 Student
(学生)。一个 Student
(学生)可以被注册在某个 Classroom
(教室)中,或者没有注册。
> grails create-domain-class Student
将领域属性添加到创建的类文件中。
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
同样,将领域属性添加到创建的类文件中。这一次,请注意 Classroom hasMany
Students(拥有很多学生)。它创建了一对多关系。
package grails.mock.basics
import groovy.transform.CompileStatic
@CompileStatic
class Classroom {
String teacher
static hasMany = [students: Student]
}
3.2 Gorm Hibernate
我们使用 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 | 在此指南中,我们使用 GORM Hibernate 实现 |
3.3 DynamicFinder 服务
下一个服务使用 GORM 动态查找器 来获取成绩高于某个阈值的的学生列表。
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
我们打算使用 HibernateSpec
在动态查找器中进行单元测试。它允许在 Grails 单元测试中使用 Hibernate。它使用 H2 内存数据库。
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 | 测试有限的领域类,覆盖 getDomainClasses 方法,并精确指定要测试的类。 |
2 | 如果待测试服务使用 @Transactional ,则需要在单元测试的设置方法中分配事务管理器。 |
3.5 投影查询
下一个服务使用带投影的 Criteria 查询来计算注册在某个教室(由教师姓名标识)的学生的平均成绩。
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 服务集成测试
我们可以使用单元测试对项目查询进行单元测试。参见
.src/test/groovy/grails/mock/basics/ClassroomServiceSpec.groovy
但是,你可能想使用集成测试。例如,你可能想在与生产环境中相同数据库上对查询进行测试。
只要使用 @Autowired
将服务注入到集成测试中,如下所示
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 具有合作者的服务
我们有一个与另一个服务合作执行某个操作的服务。这是一个常见的用例。我们打算学习如何模拟合作者,以孤立地测试服务逻辑。
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 使用模拟协作程序测试电子邮件
Spock 框架提供了模拟协作程序的功能。我们能够用模拟实现来替换某服务中的依赖项。这有助于专注于待测功能,同时将依赖功能委托给模拟对象。此外,模拟协作程序还能够验证它们被调用的次数以及发送了什么参数。
实现使用模拟协作程序来测试电子邮件的代码。基本上,我们想要验证电子邮件服务对 Classroom
中的每个 Student
都进行了调用。
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 | 模拟域类 |
2 | 实例化 EmailService 的模拟实现。 |
3 | 将模拟类注入到该服务中。 |
4 | 调用待测试服务,该服务现在有 EmailService 的模拟实现。 |
5 | 使用 spock 语法来验证调用、参数并返回一个值。 |
在 then:
子句中,我们使用 spock 来验证模拟的调用。1 *
表示验证是否调用了一次。所定义的参数验证在执行期间传入的内容。>> 1
给予模拟桩的能力,使其返回 1。
基本上,我们验证模拟被调用了三次,并带有不同的 Student
参数。
4 总结
为总结本指南,我们学习了如何...
-
使用 GORM 代码对服务的方法进行单元测试,使用
HibernateSpec
。 -
为服务创建集成测试,将服务注入
@Autowired
。 -
可以通过 Spock
Mock()
方法模拟服务。它遵循宽松模拟原则,并附带了一系列功能。
5 运行应用程序
测试方法
./grailsw
grails> test-app
grails> open test-report
或
./gradlew test