测试套件简介

目录

简介

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

在 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 的多个块。

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

在没有测试套件的情况下配置集成测试 #

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

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

Proper integration test setup without test suites

  1. 我们需要创建一个 SourceSet,它反过来会创建我们稍后需要的关联 Configurations。这是我们不应该关注的底层管道。
  2. 我们将新的测试配置连接到现有的测试配置,以重用它们的依赖项声明。我们可能并不总是想这样做。
  3. 我们需要注册一个 Test 任务来运行我们的测试。
  4. 我们应该将新任务添加到适当的组并设置描述 - 这在技术上不是必需的,但这是使任务可发现并正确将其定位在报告中的最佳实践。
  5. 我们将使用最新版本的 JUnit 5 编写测试。
  6. 我们需要设置新任务的 classpath - 这甚至更底层的管道。
  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. 我们没有通过使单元测试和集成测试的后备 Configurations 相互扩展来表示它们之间的关系,而是保持驱动两种类型测试的机制完全分离。这可以更精细地控制每种测试所需的依赖项,并避免在测试类型之间泄漏不必要的依赖项。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 源集派生的 Configurations,可通过套件自己的 dependencies 块访问:integrationTestImplementationintegrationTestCompileOnlyintegrationTestRuntimeOnly,它们的工作方式类似于它们名称相似的 test 配置。
  • 将编译和针对默认测试框架(即 JUnit Jupiter)运行时所需的依赖项添加到这些配置中。
  • 注册一个名为 integrationTestTest 任务,该任务将运行这些测试。最重要的区别是,使用测试套件将完全隔离任何集成测试依赖项与任何单元测试依赖项。它还假定 JUnit Platform 作为新测试套件的测试引擎,除非另有说明。4

在仅添加这个最小的 DSL 块之后,您就可以在 src/integrationTest/java 下编写与您的单元测试完全分离的集成测试类,并通过新的 integrationTest 任务运行它们。无需接触像 configurations 这样的底层 DSL 块。

立即试用 #

在我们探索和完善 API 时,测试套件仍然是一个 @Incubating 功能,但它将长期存在,我们鼓励所有人立即试用。对于新项目,最简单的入门方法是使用 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,这应该在您最喜欢的 IDE 中声明测试依赖项时提供更好的体验。请关注我们的发布页面了解更多详情。 

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

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

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

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

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

讨论