介绍测试套件

随着项目规模和复杂性的增长以及其他方面的成熟,它们往往会积累大量自动测试。在 多个粒度级别 测试您的软件对于快速发现问题和提高开发人员效率至关重要。

Gradle 7.3(于 2021 年 11 月发布)中,Gradle 团队引入了一项名为声明式测试套件的新功能。使用此功能可以更轻松地在单个 Gradle JVM 项目中管理不同类型的测试,而无需担心底层的“管道”细节。

为什么使用测试套件?

通常情况下,无论你是否严格遵循测试驱动开发,在开发项目时,你都会在生产类旁边不断添加新的单元测试。按照惯例,对于 Java 项目,这些测试位于 src/test/java 中。

Default test directory layout

这些单元测试确保你的类从项目的生命周期开始就能够独立地正确运行。在开发的某个阶段,你将准备好测试你的类如何协同工作以创建一个更大的系统,使用集成测试。之后,作为认证项目按设计工作最后一步,你可能希望运行整个系统的端到端测试,这些测试检查功能需求,衡量性能,或以其他方式确认项目是否已准备好发布。

在这个过程中,你需要考虑很多容易出错的细节。测试套件是为了改善这种情况而创建的,它针对以下详细说明的困难。

设置额外测试时的注意事项

不同的测试目标通常涉及不同的、不兼容的模式。至少,你应该通过将测试分离到不同目录中来组织你的测试代码,每个目录对应一个目标。

Test directory layout after adding alternate test types

但是,分离源文件仅仅是开始。这些类型的测试可能需要在测试之前满足不同的前提条件,使用完全不同的运行时和编译时依赖项,或与不同的外部系统交互。用于编写和运行每组测试的测试框架本身(例如 JUnit 4 或 5、TestNG 或 Spock)可能也不同。

为了在 Gradle 中正确地建模和隔离这些差异,你需要做的不仅仅是分离测试的源文件。每组测试都需要自己的

  • 独立的 SourceSet,它将为该组提供一个独立的 Configuration,用于声明该组编译和运行时所依赖的依赖项。你希望避免跨多个测试组泄露不必要的依赖项,但仍然自动解析测试编译和执行所需的任何共享依赖项。
  • 支持使用不同的测试框架来执行每组中的测试。
  • 每组一个 Test 任务,该任务可能具有不同的任务依赖项,以提供设置和最终化需求。你可能还想阻止每种类型的测试在每次构建项目时都运行(例如,为了在认为项目已准备好发布时才运行任何长时间运行的冒烟测试)。

你在构建脚本中创建的每个组件,用于支持这些需求,都必须正确地连接到 Gradle 项目模型中,以避免出现意外行为。实现这一点很容易出错,因为它需要深入了解 Gradle 的底层概念。它还需要修改和添加 DSL 的多个块。

这很不理想;设置测试是一个单独的关注点,其配置应该与构建脚本一起存放,并且易于发现。

在没有测试套件的情况下连接集成测试

查看一个完整的示例很有帮助。在深入研究之前,花点时间思考一下如何在项目中创建一组独立的集成测试。

在 Gradle 7.2 之前,设置集成测试的正确方法如下(注意,虽然此示例是用 Gradle Kotlin DSL 编写的,但 Groovy 设置非常相似)

Proper integration test setup without test suites

  1. 我们需要创建一个 SourceSet,它将反过来创建我们稍后需要的关联 Configuration。这是我们不应该关注的底层管道。
  2. 我们将新的测试配置连接到现有的测试配置,以重复使用它们的依赖项声明。我们可能并不总是想这样做。
  3. 我们需要注册一个 Test 任务来运行我们的测试。
  4. 我们应该将新任务添加到适当的组并设置描述 - 从技术上讲不是必需的,但最佳实践是使任务可发现并在报告中正确定位它。
  5. 我们将使用最新版本的 JUnit 5 编写测试。
  6. 我们需要设置新任务的类路径 - 这是更底层的管道。
  7. 我们需要告诉新任务它运行的类在哪里。
  8. 最后,我们将必要的 JUnit 依赖项添加到内置的测试配置中,我们的新配置扩展了这些配置。1
  9. 集成测试对当前项目的生产类具有 implementation 依赖关系 - 这在也配置项目生产依赖关系的此块中看起来有些多余。

你都明白了吗?

最重要的是,这太复杂了。你不应该仅仅为了设置彻底的测试而成为构建专家!

测试套件 - 更好的前进方向

考虑到正确处理这种情况的难度,我们意识到当前情况不足。我们希望支持构建作者以声明方式定义多个具有不同目的的测试组,同时在高抽象级别上运行。虽然您以前可以编写自己的插件(或使用现有的解决方案,例如 Nebula)来隐藏这些细节并减轻复杂性,但测试是一个如此普遍的需求,我们决定在 Gradle 自己的核心插件集中提供一个规范的解决方案。因此,测试套件诞生了。

JVM 测试套件插件 提供了 DSL 和 API 来精确地模拟这种情况:多个具有不同目的的自动化测试组,它们存在于单个基于 JVM 的项目中。它由 java 插件自动应用,因此当您升级到 Gradle >= 7.3 时,您已经在 JVM 项目中使用测试套件。恭喜!

以下是重写后的先前示例,以利用测试套件

Integration test setup with test suites

  1. 所有测试套件配置都位于一个新的 testing 块中,类型为 TestingExtension
  2. 在实现测试套件时,与已经使用 test 任务的现有构建保持向后兼容性对我们来说是一个重要的要求。我们将现有的 test 任务与一个默认的测试套件相关联,您可以使用它来包含您的单元测试。
  3. 我们没有通过让单元测试和集成测试的后台 Configuration 相互扩展来表示它们之间的关系,而是将支持两种类型测试的机制完全分开。这允许更细粒度地控制每种测试类型所需的依赖项,并避免在测试类型之间泄漏不必要的依赖项。2
  4. 由于每个测试套件可能服务于非常不同的目的,我们不假设它们依赖于您的项目(也许您正在测试外部系统),因此您必须显式添加一个依赖项才能在测试中访问您的生产类。3

通过利用合理的默认值,Gradle 可以显著简化您的构建脚本。此脚本设法建立了一个与原始构建基本等效的构建,但代码行数要少得多。Gradle 添加了一个目录来定位您的测试代码,并创建了使用套件名称作为基础运行测试的任务。在这种情况下,您可以通过调用 gradlew integrationTest 来运行位于 src/integrationTestJava 中的任何测试。

测试套件也不限于 Java 项目。Groovy、Kotlin、Scala 和其他基于 JVM 的语言在应用了相应的插件后,将以类似的方式工作。这些插件也都自动应用了 JVM 测试套件插件,因此您可以开始将测试添加到 src/<SUITE_NAME>/<LANGUAGE_NAME> 中,而无需进行任何其他配置。

幕后

这个简短的示例处理了上面预测试套件示例中所有考虑因素。
但它是如何工作的呢?

当您在新 DSL 中配置套件时,Gradle 会为您执行以下操作

  • 创建一个名为 integrationTest 的测试套件(类型为 JvmTestSuite)。
  • 创建一个名为 integrationTestSourceSet,其中包含源目录 src/java/integrationTest。这将被注册为测试源目录,因此您最喜欢的 IDE 执行的任何突出显示都将按预期工作。
  • 创建几个从 integrationTest 源集派生的 Configuration,可以通过套件自己的 dependencies 块访问:integrationTestImplementationintegrationTestCompileOnlyintegrationTestRuntimeOnly,它们的工作方式类似于它们同名的 test 配置。
  • 将依赖项添加到这些配置中,这些依赖项对于编译和针对默认测试框架(即 JUnit Jupiter)运行是必需的。
  • 注册一个名为 integrationTestTest 任务,该任务将运行这些测试。最重要的区别是,使用测试套件将完全隔离任何集成测试依赖项与任何单元测试依赖项。它还假设 JUnit Platform 作为新测试套件的测试引擎,除非另有说明。4

只需添加这个最小的 DSL 代码块,您就可以在 src/integrationTest/java 下编写与单元测试完全分离的集成测试类,并通过新的 integrationTest 任务运行它们。无需与低级 DSL 代码块(如 configurations)进行交互。

现在就试试吧

测试套件仍然是一个 @Incubating 特性,因为我们正在探索和完善 API,但它会一直存在,我们鼓励每个人现在就尝试一下。对于新项目,最简单的入门方法是使用 Gradle 初始化任务,并在提示时选择加入使用孵化特性;这将使用新的 DSL 生成一个示例项目。

自定义您的套件

测试套件背后的原理,就像 Gradle 一样,是抽象配置细节并使用合理的约定作为默认值,但同时也允许您根据需要更改这些默认值。

Customizing test suites

  1. 使用 几种可用的便捷方法 配置内置测试套件以使用不同的测试框架。5
  2. 添加一个非项目依赖项,用于编译和运行测试套件。
  3. 添加一个仅在运行测试时使用的依赖项(在本例中,是一个日志记录实现)。
  4. testing 代码块中,访问将为该套件创建的 integrationTest 任务,以直接(并且延迟地6)配置它。
  5. 使用默认新测试套件的最小 DSL 定义一个额外的 performanceTest 套件。请注意,该套件将无法访问项目的自己的类,也无法连接到构建中运行,除非直接调用其 performanceTest 任务。7
  6. 套件可以用作任务依赖项 - 这将导致 check 任务依赖于与 integrationTest 测试套件关联的 integrationTest 任务 - 与我们在 <4> 中配置的相同任务。

有关更多 Test Suite 自定义配置示例,请参阅 Gradle 用户指南中的 JVM Test Suite 插件 部分。有关将额外的测试套件添加到更复杂和现实的构建中的信息,请参阅 多项目示例

Gradle 中测试的未来

我们对未来 Test Suites 的发展有很多令人兴奋的想法。我们想要支持的一个主要用例是多维测试,其中同一套测试在不同的环境中重复运行(例如,在不同版本的 JVM 上)。这就是为什么在示例和用户指南中看到看似多余的 targets 块的原因。这样做可能需要将 Test Suite 与 JVM 工具链 更紧密地集成。

您还需要查看 Gradle 7.4 中添加的 测试报告聚合插件,了解如何轻松地将多个 Test 任务调用的结果聚合到单个 HTML 报告中。在测试报告中整合测试失败并公开更多关于其套件的信息是未来发展的另一个潜在领域。

这些和其他改进目前正在讨论和实施中,因此请务必关注此博客和最新的 Gradle 版本。

祝您测试愉快!

  1. junit-jupiter-engine 中省略了版本,因为 junit-jupiter-api 将负责设置它 - 但这可能看起来像个错误。 

  2. 在即将发布的 Gradle 8.0 中,此块将使用新的强类型 dependencies API,这将为您最喜欢的 IDE 在声明测试依赖项时提供更好的体验。请访问我们的 发布 页面以获取更多详细信息。 

  3. 请注意,在即将发布的 Gradle 7.6 中,您需要调用新的 project() 方法,而不是使用 project。 

  4. 出于兼容性原因,默认测试套件保留 JUnit 4 作为其默认运行器。 

  5. 出于相同原因,默认测试套件还隐式地被授予访问生产源代码的实现依赖项的权限。当切换测试框架时,新框架的依赖项会自动包含。 

  6. 有关延迟配置的更多详细信息,请参阅用户指南中的 延迟配置 部分。 

  7. 也许这些测试旨在对暂存环境中的实时部署进行测试。 

讨论