(快速参考)

14 测试

版本 6.2.0

14 测试

自动化测试是 Grails 开发的一个关键方面。Grails 提供了一套丰富的测试功能,从低级别的单元测试到高级别的功能测试。本综合指南详细探讨了这些不同的测试功能。

自动测试生成

当你使用 create-generate- 命令时,Grails 会自动生成 unitintegration 测试。例如,运行如下所示的 create-controller 命令

grails create-controller com.example.simple

Grails 会在 grails-app/controllers/com/example/SimpleController.groovy 生成一个控制器,并在 src/test/groovy/com/example/SimpleControllerSpec.groovy 生成一个对应的单元测试。需要注意的是,Grails 只创建测试结构;你需要实现测试逻辑。

运行测试

要执行测试,可以使用 Gradle 检查任务

./gradlew check

此命令将执行 src/main/groovy/com/example/ 目录中的所有单元测试。

目标测试

要选择性地定位要执行的测试,您有几个选项

  1. 要运行名为 SimpleController 的控制器的所有测试,请使用此命令

    ./gradlew check --tests SimpleController
  2. 要测试所有以 Controller 结尾的类,可以使用通配符

    ./gradlew check --tests *Controller
  3. 要指定包名称

    ./gradlew check --tests some.org.*Controller
  4. 要运行包中的所有测试

    ./gradlew check --tests some.org.*
  5. 要运行包中的所有测试,包括子包

    ./gradlew check --tests some.org.**.*
  6. 要定位特定的测试方法

    ./gradlew check --tests SimpleController.testLogin

您可以根据需要组合多个模式

./gradlew check --tests some.org.* SimpleController.testLogin BookController
您可能需要在类名之前指定包名,并在其后追加“Spec”。例如,要运行 ProductController 的测试,请使用 ./gradlew test *.ProductControllerSpec。如果要避免键入整个包层次结构,也可以使用星号通配符。

调试

要使用远程调试器调试测试,可以在任何命令的 ./gradlew 之后添加 --debug-jvm,如下所示

./gradlew check --debug-jvm

这将打开默认的 Java 远程调试端口 5005,允许您从代码编辑器或集成开发环境附加远程调试器。

定位测试阶段/分别运行单元测试和集成测试

要执行“单元”测试,请使用此命令

./gradlew test

对于“集成”测试,您将运行

./gradlew integrationTest

在使用阶段时定位测试

您可以组合测试和阶段定位

./gradlew test some.org.**.*

此命令将运行 some.org 包或其子包中单元阶段的所有测试。有关更详细的信息,建议查阅 Gradle 文档中关于 在 Java 和 JVM 项目中进行测试 的内容。

14.1 单元测试

单元测试是在“单元”级别进行的测试。换句话说,您正在测试单个方法或代码块,而不考虑周围的基础结构。单元测试通常在没有涉及 I/O 的物理资源(例如数据库、套接字连接或文件)的情况下运行。这是为了确保它们尽可能快地运行,因为快速反馈很重要。

从 Grails 3.3 开始,Grails 测试支持框架 用于所有单元测试。此支持提供了一组特征。下面是一个 hello world 测试示例

import spock.lang.Specification
import grails.testing.web.controllers.ControllerUnitTest

class HelloControllerTests extends Specification implements ControllerUnitTest<HelloController> {

    void "Test message action"() {
        when:"The message action is invoked"
        controller.message()

        then:"Hello is returned"
        response.text == 'Hello'
    }
}

有关使用 Grails 测试支持编写测试的更多信息,请参阅专用文档

低于 3.2 版本的 Grails 使用 Grails 测试混合框架,该框架基于 @TestMixin AST 转换。该库已被更简单、更 IDE 友好的基于特征的实现所取代。

14.2 集成测试

集成测试与单元测试的不同之处在于,您可以在测试中完全访问 Grails 环境。您可以使用 create-integration-test 命令创建集成测试

$ grails create-integration-test Example

上述命令将在 src/integration-test/groovy/<PACKAGE>/ExampleSpec.groovy 位置创建一个新的集成测试。

Grails 对集成测试使用测试环境,并在第一次测试运行之前加载应用程序。所有测试都使用相同的应用程序状态。

交易

默认情况下,集成测试方法在其自己的数据库事务中运行,该事务在每个测试方法结束时回滚。这意味着在测试期间保存的数据不会持久化到数据库(数据库在所有测试之间共享)。默认生成的集成测试模板包括 Rollback 注解

import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import spock.lang.*

@Integration
@Rollback
class ExampleSpec extends Specification {

    ...

    void "test something"() {
        expect:"fix me"
            true == false
    }
}

Rollback 注解确保每个测试方法都在回滚的事务中运行。通常情况下,这是可取的,因为您不希望测试依赖于顺序或应用程序状态。

在 Grails 3.0 中,测试依赖于 grails.gorm.transactions.Rollback 注解来绑定集成测试中的会话。尽管每个测试方法事务都会回滚,但 setup() 方法使用单独的事务,该事务不会回滚。数据将持久化到数据库,如果 setup() 设置数据并按如下所示的示例持久化它们,则需要手动清理数据

import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import spock.lang.*

@Integration
@Rollback
class BookSpec extends Specification {

    void setup() {
        // Below line would persist and not roll back
        new Book(name: 'Grails in Action').save(flush: true)
    }

    void "test something"() {
        expect:
        Book.count() == 1
    }
}

要预加载数据库并自动回滚设置逻辑,需要从测试方法本身调用任何持久化操作,以便它们可以在测试方法的回滚事务中运行。类似于使用 setupData() 方法,如下所示,该方法在数据库中创建记录,并在运行其他测试后回滚

import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import spock.lang.*

@Integration
@Rollback
class BookSpec extends Specification {

    void setupData() {
        // Below line would roll back
        new Book(name: 'Grails in Action').save(flush: true)
    }

    void "test something"() {
        given:
        setupData()

        expect:
        Book.count() == 1
    }
}

使用 Spring 的 Rollback 注解

另一种事务性方法是使用 Spring 的 @Rollback

import grails.testing.mixin.integration.Integration
import org.springframework.test.annotation.Rollback
import spock.lang.*

@Integration
@Rollback
class BookSpec extends Specification {

    void setup() {
        new Book(name: 'Grails in Action').save(flush: true)
    }

    void "test something"() {
        expect:
        Book.count() == 1
    }
}
无法使 grails.gorm.transactions.Rollback 的行为与 Spring 的 Rollback 注解相同,因为 grails.gorm.transactions.Rollback 转换了类的字节码,从而无需代理(Spring 的版本需要代理)。这样做的缺点是,您不能针对不同的情况以不同的方式实现它(就像 Spring 对测试所做的那样)。

DirtiesContext

如果您确实有一系列将共享状态的测试,则可以删除 Rollback,并且套件中的最后一个测试应具有 DirtiesContext 注解,该注解将关闭环境并重新启动它(请注意,这将影响测试运行时间)。

自动装配

要获取对 bean 的引用,可以使用 Autowired 注解。例如

...
import org.springframework.beans.factory.annotation.*

@Integration
@Rollback
class ExampleServiceSpec extends Specification {

    @Autowired
    ExampleService exampleService
    ...

    void "Test example service"() {
        expect:
            exampleService.countExamples() == 0
    }
}

测试控制器

要对控制器进行集成测试,建议您使用 create-functional-test 命令创建 Geb 功能测试。有关更多信息,请参阅以下有关功能测试的部分。

14.3 功能测试

功能测试涉及针对正在运行的应用程序发出 HTTP 请求并验证结果行为。这对于端到端测试场景非常有用,例如针对 JSON API 发出 REST 调用。

默认情况下,Grails 支持使用 Geb 框架 编写功能测试。要创建功能测试,可以使用 create-functional-test 命令,该命令将创建一个新的功能测试

$ grails create-functional-test MyFunctional

上述命令将在 src/integration-test/groovy 目录中创建一个名为 MyFunctionalSpec.groovy 的新 Spock 规范。该测试使用 Integration 注解进行注解,以指示它是一个集成测试,并扩展了 GebSpec 超类

@Integration
class HomeSpec extends GebSpec {

    def setup() {
    }

    def cleanup() {
    }

    void "Test the home page renders correctly"() {
        when:"The home page is visited"
            go '/'

        then:"The title is correct"
            $('title').text() == "Welcome to Grails"
    }
}

运行测试时,应用程序容器将在后台加载,您可以使用 Geb API 向正在运行的应用程序发送请求。

请注意,应用程序仅在整个测试运行期间加载一次,因此功能测试会在整个套件中共享应用程序的状态。

此外,应用程序作为测试加载到 JVM 中,这意味着测试可以完全访问应用程序状态,并且可以直接与数据服务(例如 GORM)交互以设置和清理测试数据。

Integration 注解支持可选的 applicationClass 属性,该属性可用于指定用于功能测试的应用程序类。该类必须扩展 GrailsAutoConfiguration

@Integration(applicationClass=com.demo.Application)
class HomeSpec extends GebSpec {

    // ...

}

如果未指定 applicationClass,则测试运行时环境将尝试动态定位应用程序类,这在可能存在多个应用程序类的多项目构建中可能会出现问题。

运行服务器时,默认情况下将随机分配端口。Integration 注解将 serverPort 属性添加到测试类中,如果您想知道应用程序正在哪个端口上运行,则可以使用该属性,如果您按上述方式扩展 GebSpec,则不需要该属性,但它可能是有用的信息。

如果要在固定端口(由 server.port 配置属性定义)上运行测试,则需要使用 @SpringBootTest 手动注解测试

import grails.testing.mixin.integration.Integration
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@Integration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class MySpec extends Specification {

    // ...

}