显示导航

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

若要开始,请执行以下操作

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

将领域属性添加到创建的类文件中。

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 hasMany 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 6.1.6.RELEASE。

gradle.properties
grailsVersion=3.3.0
grailsWrapperVersion=1.0.0
gormVersion=6.1.6.RELEASE
gradleWrapperVersion=3.5
build.gradle
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 动态查找器 来获取成绩高于某个阈值的的学生列表。

grails-app/services/grails/mock/basics/StudentService.groovy
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 内存数据库。

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 投影查询

下一个服务使用带投影的 Criteria 查询来计算注册在某个教室(由教师姓名标识)的学生的平均成绩。

grails-app/services/grails/mock/basics/ClassroomService.groovy
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 将服务注入到集成测试中,如下所示

src/integration-test/groovy/grails/mock/basics/ClassroomServiceIntegrationSpec.groovy
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 具有合作者的服务

我们有一个与另一个服务合作执行某个操作的服务。这是一个常见的用例。我们打算学习如何模拟合作者,以孤立地测试服务逻辑。

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 使用模拟协作程序测试电子邮件

Spock 框架提供了模拟协作程序的功能。我们能够用模拟实现来替换某服务中的依赖项。这有助于专注于待测功能,同时将依赖功能委托给模拟对象。此外,模拟协作程序还能够验证它们被调用的次数以及发送了什么参数。

实现使用模拟协作程序来测试电子邮件的代码。基本上,我们想要验证电子邮件服务对 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 模拟域类
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