引入测试套件

目录

引言

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

在 2021 年 11 月发布的Gradle 7.3 中,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 块。

这 hardly 是理想的;设置测试是一个单一的问题,并且它的配置应该在构建脚本中同址且易于发现。

未使用测试套件编写集成测试 #

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

在 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 依赖项——这在也配置项目生产依赖项的此块中看起来有些多余。

您都明白了amssymb?

底线是这太复杂了。 **仅仅为了设置全面的测试,您就不应该成为构建专家!**

测试套件——更好的前进方向 #

考虑到正确处理这种情况的困难,我们意识到当前的情况不足。我们希望支持构建作者以声明性方式定义具有不同目的的多个测试组,同时在高级抽象级别上运行。尽管您以前可以编写自己的插件(或使用像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 执行的任何高亮显示都会按预期工作。
  • 创建几个 Configuration,这些配置源自 integrationTest 源集,可通过套件自己的 dependencies 块访问: integrationTestImplementationintegrationTestCompileOnlyintegrationTestRuntimeOnly,它们的功能与同名的 test 配置类似。
  • 将必要的依赖项添加到这些配置中,以用于编译和运行默认测试框架(即 JUnit Jupiter)。
  • 注册一个名为 integrationTestTest 任务,该任务将运行这些测试。最重要的区别是,使用测试套件将完全隔离任何集成测试依赖项与任何单元测试依赖项。它还假设 JUnit Platform 是新测试套件的测试引擎,除非另有说明。4

在添加了如此最小的 DSL 块之后,您就可以在 src/integrationTest/java 下编写与您的单元测试完全隔离的集成测试类,并通过新的 integrationTest 任务运行它们。无需与 configurations 等低级 DSL 块进行交互。

立即尝试 #

测试套件仍然是一个@Incubating 功能,因为我们正在探索和完善 API,但它将长期存在,我们鼓励大家立即尝试。对于新项目,最简单的方法是使用Gradle Init 任务并在提示时选择使用孵化功能;这将生成一个使用新 DSL 的示例项目。

自定义您的套件 #

测试套件的根本原因,就像 Gradle 的整体一样,是抽象配置的细节并使用合理的约定作为默认值——但也允许您在必要时更改这些默认值。

Customizing test suites

  1. 使用提供的几种便捷方法之一来配置内置测试套件以使用不同的测试框架。5
  2. 添加一个非项目依赖项,用于编译和运行测试套件。
  3. 添加一个仅在运行测试时使用的依赖项(在此示例中为日志实现)。
  4. 访问将为此套件创建的 integrationTest 任务,以便在 testing 块中直接(并且惰性地6)配置它。
  5. 使用默认新测试套件的最小 DSL,定义一个附加的 performanceTest 套件。请注意,此套件将无法访问项目自身的类,或者在调用其 performanceTest 任务之前不会被连接以作为构建的一部分运行。7
  6. 可以将套件用作任务依赖项——这将导致 check 任务依赖于与 integrationTest 测试套件关联的 integrationTest 任务——即我们在 <4> 中配置的同一个任务。

有关更多测试套件自定义配置示例,请参阅 Gradle 用户指南的JVM 测试套件插件部分。有关为更复杂和更实际的构建添加附加测试套件,请参阅多项目示例

Gradle 中测试的未来 #

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

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

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

祝您测试愉快!

  1. 版本号从 junit-jupiter-engine 中省略了,因为 junit-jupiter-api 会管理设置——但这可能看起来像个错误。 

  2. 在即将发布的 Gradle 8.0 中,此块将使用新的强类型 dependencies API,该 API 在您喜欢的 IDE 中声明测试依赖项时应提供更好的体验。请关注我们的发布页面了解更多详情。 

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

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

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

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

  7. 也许这些测试是为了在暂存环境中测试实时部署。 

讨论