显示导航

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 如何完成指南

要开始,请执行以下操作

Grails 指南仓库包含两个文件夹

  • initial 初始项目。通常是一个带有额外代码的简单 Grails 应用程序,以便让您快速入门。

  • complete 一个已完成的示例。它是完成本指南介绍的步骤并将这些更改应用到 initial 文件夹的结果。

要完成指南,请转到 initial 文件夹

  • cdgrails-guides/grails-mock-basics/initial

按照下一部分中的说明进行操作。

如果你将 cd 切换至 grails-guides/grails-mock-basics/complete,即可直接转到完成示例

3 编写应用程序

我们将编写一个涉及 ClassroomStudent 域类的简单应用程序。

多个服务将与这些域类互动。我们将使用这些服务讨论多个测试主题。

3.1 域类

我们将在应用程序中创建一个 Student(学生)和 Classroom(教室)域类作为基础。ClassroomStudent 具有多对一的关系,其中 Classroom 容纳多个 Student(学生)。一个 Student(学生)可以注册在一个 Classroom(教室)或没有班级。

> grails create-domain-class Student

将域属性添加到新建类文件中。

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

同样,将域属性添加到新建类文件中。此时请注意 Classroom 可以容纳多个 Students。这创建了一个多对一的关系。

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

我们正在使用 GORM 7.1.0。

gradle.properties
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
build.gradle
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 列表。

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

我们将使用 HibernateSpec 来对动态查找程序进行单元测试。它允许在 Grails 单元测试中使用 Hibernate。它使用 H2 内存数据库。

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 测试一组有限的域类,覆盖 getDomainClasses 方法,并准确指定要测试的类。
2 如果要测试的服务使用 @Transactional,则需要在单元测试的设置方法中分配事务管理器。

3.5 投影查询

下一项服务使用带投影的条件查询来计算在某一教室中(按教师姓名进行标识)注册的学生的平均成绩。

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 服务集成测试

我们可以使用单元测试对项目查询进行单元测试。请参见

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

但是,你可能想使用集成测试。例如,你可能希望针对你在生产环境中使用的相同数据库来测试查询。

只需使用 @Autowired 将你的服务注入到集成测试中,如下所示

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 具有合作者的服务

我们有一个服务与另一项服务协作以执行一项操作。这是一个常见用例。我们将学习如何模拟合作者来隔离地测试服务逻辑。

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 使用 Mock 协作者测试电子邮件

Spock 框架为 Mock 协作者提供了功能。我们能够用 Mock 实现来替换服务中的依赖项。这提供了关注于测试功能的优势,同时将依赖的功能委派给某个 Mock 对象。此外,可以验证 Mock 协作者,以查看它们被调用了多少次以及发送了哪些参数。

实施使用 Mock 协作者测试电子邮件的代码。从本质上讲,我们希望验证电子邮件服务是否为 Classroom 中的每个 Student 被调用。

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 域类
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