Grails 服务测试
在本指南中,我们将探索 Grails 中的服务测试。单元测试 GORM、集成测试、模拟协作者...
作者:Nirav Assar
Grails 版本 5.0.1
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和交付!。
2 开始
在本指南中,您将探索 Grails 中的服务测试。您将学习以下内容
-
Grails 服务单元测试 - 模拟协作者
-
Grails 服务单元测试 - GORM 代码
-
Grails 服务集成测试
2.1 需要什么
要完成本指南,您需要具备以下条件
-
一些空闲时间
-
一个文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,并恰当地配置了
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆Git 仓库
git clone https://github.com/grails-guides/grails-mock-basics.git
Grails 指南仓库包含两个文件夹
-
initial
初始项目。通常是一个带有额外代码的简单 Grails 应用程序,以便让您快速入门。 -
complete
一个已完成的示例。它是完成本指南介绍的步骤并将这些更改应用到initial
文件夹的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-mock-basics/initial
按照下一部分中的说明进行操作。
如果你将 cd 切换至 grails-guides/grails-mock-basics/complete ,即可直接转到完成示例 |
3 编写应用程序
我们将编写一个涉及 Classroom
和 Student
域类的简单应用程序。
多个服务将与这些域类互动。我们将使用这些服务讨论多个测试主题。
3.1 域类
我们将在应用程序中创建一个 Student
(学生)和 Classroom
(教室)域类作为基础。Classroom
和 Student
具有多对一的关系,其中 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
可以容纳多个 Students
。这创建了一个多对一的关系。
package grails.mock.basics
import groovy.transform.CompileStatic
@CompileStatic
class Classroom {
String teacher
static hasMany = [students: Student]
}
3.2 Gorm Hibernate
我们正在使用 GORM 7.1.0。
grailsVersion=5.0.1
grailsGradlePluginVersion=5.0.0
gormVersion=7.1.0
groovyVersion=3.0.7
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M
buildscript {
...
dependencies {
...
classpath "org.grails.plugins:hibernate5:7.1.0" (1)
}
}
...
dependencies {
...
implementation "org.grails.plugins:hibernate5" (1)
implementation "org.hibernate:hibernate-core:5.5.7.Final" (1)
}
}
1 | 针对本指南,我们正在使用 GORM Hibernate 实施 |
3.3 DynamicFinder 服务
下一项服务使用 GORM 动态查找程序来获取成绩高于某个阈值的 Students
列表。
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
我们将使用 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 投影查询
下一项服务使用带投影的条件查询来计算在某一教室中(按教师姓名进行标识)注册的学生的平均成绩。
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 服务集成测试
我们可以使用单元测试对项目查询进行单元测试。请参见
.src/test/groovy/grails/mock/basics/ClassroomServiceSpec.groovy
但是,你可能想使用集成测试。例如,你可能希望针对你在生产环境中使用的相同数据库来测试查询。
只需使用 @Autowired
将你的服务注入到集成测试中,如下所示
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 具有合作者的服务
我们有一个服务与另一项服务协作以执行一项操作。这是一个常见用例。我们将学习如何模拟合作者来隔离地测试服务逻辑。
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 使用 Mock 协作者测试电子邮件
Spock 框架为 Mock 协作者提供了功能。我们能够用 Mock 实现来替换服务中的依赖项。这提供了关注于测试功能的优势,同时将依赖的功能委派给某个 Mock 对象。此外,可以验证 Mock 协作者,以查看它们被调用了多少次以及发送了哪些参数。
实施使用 Mock 协作者测试电子邮件的代码。从本质上讲,我们希望验证电子邮件服务是否为 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 | Mock 域类 |
2 | 实例化 EmailService 的 Mock 实现。 |
3 | 将 Mock 注入到服务中。 |
4 | 调用正在接受测试的服务,该服务现在具有 EmailService 的 Mock 实现。 |
5 | 使用 Spock 语法来验证调用、参数和返回的值。 |
在 then:
子句中,我们使用 Spock 来验证是否调用了 Mock。使用 1 *
验证是否调用了一次。定义的参数验证在执行期间传入的内容。>> 1
提供 Mock stub 功能,它将返回 1。
从本质上讲,我们正在验证使用不同的 Student
参数调用了 Mock 三次。
4 总结
为了总结本指南,我们学习了如何...
-
使用
HibernateSpec
对具有 GORM 代码的服务的方法进行单元测试。 -
为某个服务创建集成测试,使用
@Autowired
来注入该服务。 -
使用 Spock
Mock()
方法可以 Mock 服务。它遵循宽松的 Mock 原则,并附带大量功能。
5 运行应用程序
要运行测试
./grailsw
grails> test-app
grails> open test-report
或
./gradlew test
要运行集成测试
./gradlew integrationTest