JVM 上的测试是一个丰富的主题。存在许多不同的测试库和框架,以及许多不同类型的测试。所有这些都需要成为构建的一部分,无论它们是频繁执行还是不频繁执行。本章专门解释了 Gradle 如何处理构建之间和内部的不同需求,并重点介绍了它如何与两个最常见的测试框架:JUnitTestNG 集成。

本章解释了

但首先,让我们看看 Gradle 中 JVM 测试的基础知识。

通过孵化中的 JVM 测试套件 (Test Suite) 插件,可以使用新的配置 DSL 来建模测试执行阶段。

基础知识

所有 JVM 测试都围绕着一个任务类型:Test。它使用任何支持的测试库(JUnit、JUnit Platform 或 TestNG)运行一系列测试用例,并整理结果。然后,你可以通过 TestReport 任务类型的实例将这些结果转化为报告。

为了运行,Test 任务类型只需要两部分信息:

当你使用 JVM 语言插件时(例如 Java 插件),你将自动获得以下内容:

  • 用于单元测试的专用 test 源集 (source set)

  • 一个类型为 Testtest 任务,用于运行这些单元测试

JVM 语言插件使用源集来配置任务,提供适当的执行类路径和包含编译后测试类的目录。此外,它们还将 test 任务附加到 check 生命周期任务上。

还需要记住的是,test 源集会自动创建相应的依赖配置——其中最有用的是 testImplementationtestRuntimeOnly——插件会将这些配置关联到 test 任务的类路径上。

在大多数情况下,你只需要配置相应的编译和运行时依赖项,并向 test 任务添加任何必要的配置。以下示例展示了一个使用 JUnit Platform 并将测试 JVM 的最大堆大小更改为 1 GB 的简单设置:

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.named<Test>("test") {
    useJUnitPlatform()

    maxHeapSize = "1G"

    testLogging {
        events("passed")
    }
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test', Test) {
    useJUnitPlatform()

    maxHeapSize = '1G'

    testLogging {
        events "passed"
    }
}

Test 任务有许多通用配置选项,以及一些框架特定的选项,你可以在 JUnitOptionsJUnitPlatformOptionsTestNGOptions 中找到描述。本章的其余部分将涵盖其中相当一部分。

如果你想设置自己的 Test 任务,并使用它自己的测试类集,那么最简单的方法是创建自己的源集和 Test 任务实例,如配置集成测试中所述。

测试执行

Gradle 在一个独立的(“forked”)JVM 中执行测试,与主构建过程隔离。这可以防止类路径污染和构建过程过度消耗内存。它还允许你使用与构建不同的 JVM 参数运行测试。

你可以通过 Test 任务上的几个属性控制测试进程的启动方式,包括以下内容:

maxParallelForks — 默认值: 1

你可以通过将此属性设置为大于 1 的值来并行运行测试。这可能会让你的测试套件更快地完成,尤其是在多核 CPU 上运行时。在使用并行测试执行时,请确保你的测试彼此之间正确隔离。与文件系统交互的测试特别容易发生冲突,导致间歇性测试失败。

你的测试可以通过使用 org.gradle.test.worker 属性的值来区分并行测试进程,该值对于每个进程是唯一的。你可以将此用于任何目的,但它对于文件名和其他资源标识符特别有用,可以防止我们刚才提到的那种冲突。

forkEvery — 默认值: 0 (无上限)

此属性指定 Gradle 应在处理一个测试进程并创建新进程之前运行的最大测试类数量。这主要用于管理内存泄漏测试或具有无法在测试之间清除或重置的静态状态的框架。

警告:较低的值(0 除外)可能会严重损害测试的性能

ignoreFailures — 默认值: false

如果此属性为 true,则 Gradle 将在测试完成后继续执行项目的构建,即使其中一些测试失败了。请注意,默认情况下,Test 任务始终会执行它检测到的所有测试,无论此设置如何。

failFast —  (Gradle 4.6 开始支持) 默认值: false

如果你希望构建在其中一个测试失败时立即失败并结束,请将此设置为 true。当你有一个长时间运行的测试套件时,这可以节省大量时间,并且在持续集成服务器上运行构建时特别有用。当构建在所有测试运行完成之前失败时,测试报告仅包含已完成测试的结果,无论成功与否。

你也可以通过使用 --fail-fast 命令行选项启用此行为,或使用 --no-fail-fast 禁用此行为。

testLogging — 默认值: 未设置

此属性代表一组选项,用于控制哪些测试事件以及在什么级别进行日志记录。你也可以通过此属性配置其他日志记录行为。有关更多详细信息,请参阅 TestLoggingContainer

dryRun — 默认值: false

如果此属性为 true,Gradle 将模拟测试的执行,但不会实际运行它们。这仍会生成报告,允许检查选择了哪些测试。这可以用于验证你的测试过滤配置是否正确,而无需实际运行测试。

你也可以通过使用 --test-dry-run 命令行选项启用此行为,或使用 --no-test-dry-run 禁用此行为。

有关所有可用配置选项的详细信息,请参阅 Test

如果配置不正确,测试进程可能会意外退出。例如,如果 Java 可执行文件不存在或提供了无效的 JVM 参数,测试进程将无法启动。同样,如果测试对测试进程进行编程更改,这也可能导致意外失败。

例如,如果在测试中修改了 SecurityManager,可能会出现问题,因为 Gradle 的内部消息传递依赖于反射和套接字通信,如果安全管理器上的权限发生变化,这些可能会受到干扰。在这种特定情况下,你应该在测试后恢复原始的 SecurityManager,以便 Gradle 测试 worker 进程能够继续正常工作。

测试过滤

运行测试套件的子集是一项常见需求,例如在修复 bug 或开发新测试用例时。Gradle 提供了两种机制来实现此目的:

  • 过滤(首选选项)

  • 测试包含/排除

过滤机制取代了包含/排除机制,但你可能仍然会在实际项目中遇到后者。

使用 Gradle 的测试过滤,你可以根据以下条件选择要运行的测试:

  • 完全限定类名或完全限定方法名,例如 org.gradle.SomeTest, org.gradle.SomeTest.someMethod

  • 简单类名或方法名,如果模式以大写字母开头,例如 SomeTest, SomeTest.someMethod (Gradle 4.7 开始支持)

  • '*' 通配符匹配

你可以在构建脚本中或通过 --tests 命令行选项启用过滤。以下是一些每次构建运行时都会应用的过滤示例:

build.gradle.kts
tasks.test {
    filter {
        //include specific method in any of the tests
        includeTestsMatching("*UiCheck")

        //include all tests from package
        includeTestsMatching("org.gradle.internal.*")

        //include all integration tests
        includeTestsMatching("*IntegTest")
    }
}
build.gradle
test {
    filter {
        //include specific method in any of the tests
        includeTestsMatching "*UiCheck"

        //include all tests from package
        includeTestsMatching "org.gradle.internal.*"

        //include all integration tests
        includeTestsMatching "*IntegTest"
    }
}

有关在构建脚本中声明过滤器的更多详细信息和示例,请参阅 TestFilter 参考。

命令行选项对于执行单个测试方法特别有用。使用 --tests 时,请注意构建脚本中声明的包含规则仍然有效。也可以提供多个 --tests 选项,所有这些选项的模式都会生效。以下部分包含使用命令行选项的几个示例。

并非所有测试框架都与过滤很好地兼容。一些高级的、合成的测试可能无法完全兼容。然而,绝大多数测试和用例都能与 Gradle 的过滤机制完美配合。

以下两节将讨论简单类名/方法名和完全限定名的具体情况。

简单名称模式

自 4.7 版本以来,Gradle 将以大写字母开头的模式视为简单类名,或类名 + 方法名。例如,以下命令行运行 SomeTestClass 测试用例中的所有测试或恰好一个测试,无论它位于哪个包中:

# Executes all tests in SomeTestClass
gradle test --tests SomeTestClass

# Executes a single specified test in SomeTestClass
gradle test --tests SomeTestClass.someSpecificMethod

gradle test --tests SomeTestClass.*someMethod*

完全限定名模式

在 4.7 版本之前,或者如果模式不以大写字母开头,Gradle 会将模式视为完全限定名。因此,如果你想使用测试类名而不考虑其包,你可以使用 --tests *.SomeTestClass。以下是一些更多示例:

# specific class
gradle test --tests org.gradle.SomeTestClass

# specific class and method
gradle test --tests org.gradle.SomeTestClass.someSpecificMethod

# method name containing spaces
gradle test --tests "org.gradle.SomeTestClass.some method containing spaces"

# all classes at specific package (recursively)
gradle test --tests 'all.in.specific.package*'

# specific method at specific package (recursively)
gradle test --tests 'all.in.specific.package*.someSpecificMethod'

gradle test --tests '*IntegTest'

gradle test --tests '*IntegTest*ui*'

gradle test --tests '*ParameterizedTest.foo*'

# the second iteration of a parameterized test
gradle test --tests '*ParameterizedTest.*[2]'

请注意,通配符 '*' 对 '.' 包分隔符没有特殊理解。它完全基于文本。因此 --tests *.SomeTestClass 将匹配任何包,无论其“深度”如何。

你还可以将命令行定义的过滤器与持续构建结合使用,以便在每次更改生产或测试源文件后立即重新执行测试子集。以下示例在每次更改触发测试运行时,执行“com.mypackage.foo”包或子包中的所有测试:

gradle test --continuous --tests "com.mypackage.foo.*"

测试报告

Test 任务默认生成以下结果:

  • HTML 测试报告

  • XML 测试结果,格式兼容 Ant JUnit 报告任务——许多其他工具(如 CI 服务器)都支持该格式

  • 结果的高效二进制格式,供 Test 任务用于生成其他格式

在大多数情况下,你将使用标准的 HTML 报告,它会自动包含来自所有 Test 任务的结果,即使是你自己明确添加到构建中的任务。例如,如果你为集成测试添加了一个 Test 任务,如果两个任务都运行,报告将包含单元测试和集成测试的结果。

要跨多个子项目汇总测试结果,请参阅测试报告汇总插件

与许多测试配置选项不同,有几个项目级别的约定属性会影响测试报告。例如,你可以像这样更改测试结果和报告的目标目录:

build.gradle.kts
reporting.baseDirectory = file("my-reports")
java.testResultsDir = layout.buildDirectory.dir("my-test-results")

tasks.register("showDirs") {
    val settingsDir = project.layout.settingsDirectory.asFile
    val reportsDir = project.reporting.baseDirectory
    val testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(settingsDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(settingsDir.toPath().relativize(testResultsDir.get().asFile.toPath()).toString())
    }
}
build.gradle
reporting.baseDirectory = file("my-reports")
java.testResultsDir = layout.buildDirectory.dir("my-test-results")

tasks.register('showDirs') {
    def settingsDir = project.layout.settingsDirectory.asFile
    def reportsDir = project.reporting.baseDirectory
    def testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(settingsDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(settingsDir.toPath().relativize(testResultsDir.get().asFile.toPath()).toString())
    }
}
gradle -q showDirs 命令的输出
> gradle -q showDirs
my-reports
build/my-test-results

有关更多详细信息,请点击链接查看约定属性。

还有一个独立的 TestReport 任务类型,你可以用它来生成自定义 HTML 测试报告。它只需要 destinationDir 的值以及你希望包含在报告中的测试结果。这是一个为所有子项目的单元测试生成合并报告的示例:

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
plugins {
    id("java")
}

// Disable the test report for the individual test task
tasks.named<Test>("test") {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations.create("binaryTestResultsElements") {
    isCanBeResolved = false
    isCanBeConsumed = true
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("test-report-data"))
    }
    outgoing.artifact(tasks.test.map { task -> task.getBinaryResultsDirectory().get() })
}
build.gradle.kts
val testReportData by configurations.creating {
    isCanBeConsumed = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("test-report-data"))
    }
}

dependencies {
    testReportData(project(":core"))
    testReportData(project(":util"))
}

tasks.register<TestReport>("testReport") {
    destinationDirectory = reporting.baseDirectory.dir("allTests")
    // Use test results from testReportData configuration
    testResults.from(testReportData)
}
buildSrc/src/main/groovy/myproject.java-conventions.gradle
plugins {
    id 'java'
}

// Disable the test report for the individual test task
test {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations {
    binaryTestResultsElements {
        canBeResolved = false
        canBeConsumed = true
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'test-report-data'))
        }
        outgoing.artifact(test.binaryResultsDirectory)
    }
}
build.gradle
// A resolvable configuration to collect test reports data
configurations {
    testReportData {
        canBeConsumed = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'test-report-data'))
        }
    }
}

dependencies {
    testReportData project(':core')
    testReportData project(':util')
}

tasks.register('testReport', TestReport) {
    destinationDirectory = reporting.baseDirectory.dir('allTests')
    // Use test results from testReportData configuration
    testResults.from(configurations.testReportData)
}

在这个例子中,我们使用约定插件 myproject.java-conventions 将项目的测试结果暴露给 Gradle 的变体感知 (variant aware) 依赖管理引擎

该插件声明了一个可消费的 binaryTestResultsElements 配置,该配置代表了 test 任务的二进制测试结果。在汇总项目的构建文件中,我们声明了 testReportData 配置,并依赖于所有我们希望汇总结果的项目。Gradle 将自动从每个子项目中选择二进制测试结果变体,而不是项目的 jar 文件。最后,我们添加了一个 testReport 任务,该任务汇总了来自 testResultsDirs 属性的测试结果,该属性包含了从 testReportData 配置解析的所有二进制测试结果。

你应该注意,TestReport 类型结合了多个测试任务的结果,并且需要汇总单个测试类的结果。这意味着如果同一个测试类被多个测试任务执行,则测试报告将包含该类的执行结果,但很难区分该类的单独执行及其输出。

通过 XML 文件将测试结果传达给 CI 服务器和其他工具

Test 任务以“JUnit XML”伪标准格式创建描述测试结果的 XML 文件。JUnit 4、JUnit Jupiter 和 TestNG 测试框架都使用此标准,并且使用相同的 DSL 块进行配置。CI 服务器和其他工具通常通过这些 XML 文件来查看测试结果。

默认情况下,文件会写入到 layout.buildDirectory.dir("test-results/$testTaskName"),每个测试类一个文件。可以更改项目所有测试任务的位置,或针对每个测试任务单独更改。

build.gradle.kts
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
build.gradle
java.testResultsDir = layout.buildDirectory.dir("junit-xml")

使用上述配置,XML 文件将写入到 layout.buildDirectory.dir("junit-xml/$testTaskName")

build.gradle.kts
tasks.test {
    reports {
        junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
    }
}
build.gradle
test {
    reports {
        junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
    }
}

使用上述配置,test 任务的 XML 文件将写入到 layout.buildDirectory.dir("test-results/test-junit-xml")。其他测试任务的 XML 文件位置将保持不变。

配置选项

还可以通过配置 JUnitXmlReport 选项来配置 XML 文件的内容,以不同的方式传达结果。

build.gradle.kts
tasks.test {
    reports {
        junitXml.apply {
            includeSystemOutLog = false // defaults to true
            includeSystemErrLog = false // defaults to true
            isOutputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
build.gradle
test {
    reports {
        junitXml {
            includeSystemOutLog = false // defaults to true
            includeSystemErrLog = false // defaults to true
            outputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
includeSystemOutLog 和 includeSystemErrLog

includeSystemOutLog 选项允许配置是否将写入标准输出的测试输出导出到 XML 报告文件。includeSystemErrLog 选项允许配置是否将写入标准错误的测试错误输出导出到 XML 报告文件。

这些选项会影响测试套件级别的输出(例如 @BeforeClass/@BeforeAll 输出)以及测试类和方法特定的输出(@Before/@BeforeEach@Test)。如果任一选项被禁用,通常包含该内容的元素将被排除在 XML 报告文件之外。

每个选项的默认值都是 true

outputPerTestCase

启用 outputPerTestCase 选项后,在测试用例执行期间生成的任何输出日志都将与结果中的该测试用例相关联。禁用时(默认),输出与整个测试类相关联,而不是与产生日志输出的单个测试用例(例如测试方法)相关联。大多数观察 JUnit XML 文件的现代工具都支持“按测试用例输出”格式。

如果你使用 XML 文件传达测试结果,建议启用此选项,因为它提供了更有用的报告。

mergeReruns

启用 mergeReruns 后,如果一个测试失败,但在重试后成功,其失败将被记录为 <flakyFailure>,而不是 <failure>,并包含在一个 <testcase> 中。这实际上是 Apache Maven™ 的 surefire 插件在启用重试时生成的报告。如果你的 CI 服务器理解此格式,它将指示测试不稳定 (flaky)。如果不理解,它将忽略 <flakyFailure> 信息,并指示测试成功。如果测试没有成功(即每次重试都失败),无论你的工具是否理解此格式,它都将被指示为失败。

禁用 mergeReruns 时(默认),测试的每次执行都将列为一个单独的测试用例。

如果你使用 build scansDevelocity,无论此设置如何,都会检测到不稳定测试 (flaky tests)。

当使用依赖于 XML 测试结果来判断构建是否失败(而不是依赖 Gradle 的判断)的 CI 工具,并且你希望在所有失败测试在重试后通过时不将构建视为失败时,启用此选项特别有用。Jenkins CI 服务器及其 JUnit 插件就是这种情况。启用 mergeReruns 后,重试后通过的测试将不再导致此 Jenkins 插件认为构建失败。但是,失败的测试执行将不会出现在 Jenkins 的测试结果可视化中,因为它不考虑 <flakyFailure> 信息。可以在 JUnit Jenkins 插件之外使用独立的 Flaky Test Handler Jenkins 插件,以便将此类“不稳定失败”也可视化。

测试根据其报告的名称进行分组和合并。当使用任何影响报告测试名称的测试参数化,或任何产生潜在动态测试名称的其他机制时,应注意确保测试名称是稳定的,并且不会不必要地改变。

启用 mergeReruns 选项不会为测试执行添加任何重试/重新运行功能。重新运行可以通过测试执行框架(例如 JUnit 的 @RepeatedTest)或通过独立的 Test Retry Gradle plugin 启用。

测试检测

默认情况下,Gradle 会运行它检测到的所有测试,它通过检查编译后的测试类来实现这一点。这种检测会根据使用的测试框架使用不同的标准。

对于 JUnit,Gradle 会扫描 JUnit 3 和 4 的测试类。一个类被认为是 JUnit 测试,如果它:

  • 最终继承自 TestCaseGroovyTestCase

  • 使用 @RunWith 注解

  • 包含一个带有 @Test 注解的方法,或者其父类包含这样的方法

对于 TestNG,Gradle 会扫描使用 @Test 注解的方法。

注意,抽象类不会被执行。此外,请注意 Gradle 会沿着继承树向上扫描到测试类路径上的 jar 文件中。因此,如果这些 JAR 包含测试类,它们也会被运行。

如果你不想使用测试类检测,可以通过将 Test 上的 scanForTestClasses 属性设置为 false 来禁用它。当你这样做时,测试任务将只使用 includesexcludes 属性来查找测试类。

如果 scanForTestClasses 为 false 并且没有指定包含或排除模式,Gradle 默认会运行任何匹配模式 **/*Tests.class**/*Test.class 的类,并排除匹配 **/Abstract*.class 的类。

对于 JUnit Platform,只有 includesexcludes 用于过滤测试类 — scanForTestClasses 无效。

测试日志记录

Gradle 允许对记录到控制台的事件进行精细控制。日志记录可以按日志级别进行配置,默认情况下会记录以下事件:

当日志级别为

记录的事件

附加配置

ERROR, QUIETWARNING

LIFECYCLE

测试失败

异常格式为 SHORT

INFO

测试失败, 跳过测试, 测试标准输出测试标准错误

堆栈跟踪会被截断。

DEBUG

所有事件

记录完整的堆栈跟踪。

测试日志记录可以按日志级别进行修改,方法是调整测试任务的 testLogging 属性中相应的 TestLogging 实例。例如,要调整 INFO 级别的测试日志记录配置,请修改 TestLoggingContainer.getInfo() 属性。

测试分组

JUnit、JUnit Platform 和 TestNG 允许对测试方法进行精细分组。

本节适用于在服务于相同测试目的(单元测试、集成测试、验收测试等)的测试集合中对单个测试类或方法进行分组。要根据测试类的目的进行划分,请参阅孵化中的 JVM 测试套件 (Test Suite) 插件。

JUnit 4.8 引入了类别 (categories) 的概念,用于对 JUnit 4 测试类和方法进行分组。[1] Test.useJUnit(org.gradle.api.Action) 允许你指定要包含和排除的 JUnit 类别。例如,以下配置为 test 任务包含了 CategoryA 中的测试,并排除了 CategoryB 中的测试:

示例 8. JUnit 类别
build.gradle.kts
tasks.test {
    useJUnit {
        includeCategories("org.gradle.junit.CategoryA")
        excludeCategories("org.gradle.junit.CategoryB")
    }
}
build.gradle
test {
    useJUnit {
        includeCategories 'org.gradle.junit.CategoryA'
        excludeCategories 'org.gradle.junit.CategoryB'
    }
}

JUnit Platform 引入了标记 (tagging) 来取代类别。你可以通过 Test.useJUnitPlatform(org.gradle.api.Action) 指定要包含/排除的标记,如下所示:

build.gradle.kts
tasks.withType<Test>().configureEach {
    useJUnitPlatform {
        includeTags("fast")
        excludeTags("slow")
    }
}
build.gradle
tasks.withType(Test).configureEach {
    useJUnitPlatform {
        includeTags 'fast'
        excludeTags 'slow'
    }
}

TestNG 框架使用测试组 (test groups) 的概念来实现类似的效果。[2] 你可以通过 Test.useTestNG(org.gradle.api.Action) 设置,在测试执行期间配置要包含或排除的测试组,如下所示:

build.gradle.kts
tasks.named<Test>("test") {
    useTestNG {
        val options = this as TestNGOptions
        options.excludeGroups("integrationTests")
        options.includeGroups("unitTests")
    }
}
build.gradle
test {
    useTestNG {
        excludeGroups 'integrationTests'
        includeGroups 'unitTests'
    }
}

使用 JUnit 5

JUnit 5 是知名 JUnit 测试框架的最新版本。与前身不同,JUnit 5 是模块化的,由几个模块组成:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform 作为在 JVM 上启动测试框架的基础。JUnit Jupiter 是用于在 JUnit 5 中编写测试和扩展的新编程模型扩展模型的组合。JUnit Vintage 提供了一个 TestEngine,用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。

以下代码在 build.gradle 中启用 JUnit Platform 支持:

build.gradle.kts
tasks.named<Test>("test") {
    useJUnitPlatform()
}
build.gradle
tasks.named('test', Test) {
    useJUnitPlatform()
}

有关更多详细信息,请参阅 Test.useJUnitPlatform()

编译并执行 JUnit Jupiter 测试

要在 Gradle 中启用 JUnit Jupiter 支持,您只需要添加以下依赖项

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

然后,您可以像往常一样将测试用例放在 src/test/java 中,并使用 gradle test 执行它们。

使用 JUnit Vintage 执行遗留测试

如果您想在 JUnit Platform 上运行 JUnit 3/4 测试,甚至将其与 Jupiter 测试混合,则应添加额外的 JUnit Vintage Engine 依赖项

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testCompileOnly("junit:junit:4.13")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testCompileOnly 'junit:junit:4.13'
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

通过这种方式,您可以使用 gradle test 在 JUnit Platform 上测试 JUnit 3/4 测试,而无需重写它们。

过滤测试引擎

JUnit Platform 允许您使用不同的测试引擎。JUnit 目前提供了两种开箱即用的 TestEngine 实现:junit-jupiter-enginejunit-vintage-engine。您还可以编写并插入自己的 TestEngine 实现,相关文档请参见此处

默认情况下,将使用测试运行时类路径上的所有测试引擎。要明确控制特定的测试引擎实现,可以在构建脚本中添加以下设置

build.gradle.kts
tasks.withType<Test>().configureEach {
    useJUnitPlatform {
        includeEngines("junit-vintage")
        // excludeEngines("junit-jupiter")
    }
}
build.gradle
tasks.withType(Test).configureEach {
    useJUnitPlatform {
        includeEngines 'junit-vintage'
        // excludeEngines 'junit-jupiter'
    }
}

TestNG 中的测试执行顺序

当使用 testng.xml 文件时,TestNG 允许明确控制测试的执行顺序。如果没有这样的文件——或者通过 TestNGOptions.getSuiteXmlBuilder() 配置的等效文件——您无法指定测试执行顺序。但是,您可以控制一个测试的所有方面——包括其关联的 @BeforeXXX@AfterXXX 方法,例如带有 @Before/AfterClass@Before/AfterMethod 注解的方法——是否在下一个测试开始之前执行。您可以通过将 TestNGOptions.getPreserveOrder() 属性设置为 true 来实现这一点。如果将其设置为 false,您可能会遇到执行顺序如下的场景:TestA.doBeforeClass()TestB.doBeforeClass()TestA 测试。

虽然在使用 testng.xml 文件时,保留测试顺序是默认行为,但 Gradle 的 TestNG 集成使用的 TestNG API 默认以不可预测的顺序执行测试。[3] 保留测试执行顺序的功能是在 TestNG 5.14.5 版本中引入的。将旧版本 TestNG 的 preserveOrder 属性设置为 true 将导致构建失败。

build.gradle.kts
tasks.test {
    useTestNG {
        preserveOrder = true
    }
}
build.gradle
test {
    useTestNG {
        preserveOrder = true
    }
}

groupByInstance 属性控制测试是按实例分组还是按类分组。TestNG 文档更详细地解释了其中的区别,但实质上,如果您有一个测试方法 A() 依赖于 B(),按实例分组可确保每个 A-B 配对(例如 B(1)-A(1))在下一个配对之前执行。按类分组时,所有 B() 方法都会运行,然后所有 A() 方法才会运行。

请注意,如果您使用数据提供器来参数化测试,通常才会有多个测试实例。此外,按实例分组测试的功能是在 TestNG 6.1 版本中引入的。将旧版本 TestNG 的 groupByInstances 属性设置为 true 将导致构建失败。

build.gradle.kts
tasks.test {
    useTestNG {
        groupByInstances = true
    }
}
build.gradle
test {
    useTestNG {
        groupByInstances = true
    }
}

TestNG 参数化方法和报告

TestNG 支持 参数化测试方法,允许一个特定的测试方法使用不同的输入执行多次。Gradle 在其测试方法执行报告中包含了参数值。

对于一个名为 aTestMethod 的参数化测试方法,如果它有两个参数,报告中将显示为 aTestMethod(toStringValueOfParam1, toStringValueOfParam2)。这使得轻松识别特定迭代的参数值成为可能。

配置集成测试

项目的一个常见需求是以某种形式包含集成测试。其目的是验证项目的各个部分是否正常协同工作。这通常意味着与单元测试相比,它们需要特殊的执行设置和依赖项。

将集成测试添加到构建中的最简单方法是利用正在孵化(incubating)的 JVM Test Suite 插件。如果孵化中的解决方案不适合您,以下是您需要在构建中采取的步骤

  1. 为它们创建一个新的 源代码集(source set)

  2. 将所需的依赖项添加到该源代码集的相应配置中

  3. 配置该源代码集的编译和运行时类路径

  4. 创建一个运行集成测试的任务

您可能还需要根据集成测试的形式进行一些额外配置。我们将在接下来的内容中讨论这些。

让我们从一个实际示例开始,该示例在构建脚本中实现了前三个步骤,围绕一个新的源代码集 intTest

build.gradle.kts
sourceSets {
    create("intTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

val intTestImplementation by configurations.getting {
    extendsFrom(configurations.implementation.get())
}
val intTestRuntimeOnly by configurations.getting

configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())

dependencies {
    intTestImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    intTestRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
sourceSets {
    intTest {
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
    }
}

configurations {
    intTestImplementation.extendsFrom implementation
    intTestRuntimeOnly.extendsFrom runtimeOnly
}

dependencies {
    intTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    intTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

这将设置一个名为 intTest 的新源代码集,该源代码集会自动创建

  • intTestImplementationintTestCompileOnlyintTestRuntimeOnly 配置(以及一些不太常用的其他配置)

  • 一个 compileIntTestJava 任务,它将编译 src/intTest/java 下的所有源文件

如果您使用 IntelliJ IDE,您可能希望将这些额外源代码集中的目录标记为包含测试源而不是生产源,如 Idea 插件文档中所述。

该示例还执行以下操作,其中并非所有操作都可能适用于您的特定集成测试

  • main 源代码集的生产类添加到集成测试的编译和运行时类路径中 — sourceSets.main.output 是包含所有编译后的生产类和资源的目录的文件集合

  • 使 intTestImplementation 配置扩展自 implementation,这意味着所有声明的生产代码依赖项也成为集成测试的依赖项

  • intTestRuntimeOnly 配置也执行相同的操作

在大多数情况下,您希望集成测试能够访问被测类,这就是我们在本示例中确保将它们包含在编译和运行时类路径上的原因。但某些类型的测试与生产代码的交互方式不同。例如,您可能有一些测试是将您的应用程序作为可执行文件运行并验证输出。对于 Web 应用程序,测试可能通过 HTTP 与您的应用程序交互。由于在这种情况下测试不需要直接访问被测类,您无需将生产类添加到测试类路径中。

另一个常见步骤是将所有单元测试依赖项也附加到集成测试中——通过 intTestImplementation.extendsFrom testImplementation——但这只有在集成测试需要与单元测试完全相同或几乎所有相同的依赖项时才有意义。

示例中还有几个您应该注意的方面

  • += 允许您将路径和路径集合追加到 compileClasspathruntimeClasspath,而不是覆盖它们

  • 如果要使用基于约定的配置(例如 intTestImplementation),您必须在声明新源代码集之后声明依赖项

创建和配置源代码集会自动设置编译阶段,但对运行集成测试没有任何作用。因此,最后一块拼图是一个自定义测试任务,它使用新源代码集的信息来配置其运行时类路径和测试类

build.gradle.kts
val integrationTest = task<Test>("integrationTest") {
    description = "Runs integration tests."
    group = "verification"

    testClassesDirs = sourceSets["intTest"].output.classesDirs
    classpath = sourceSets["intTest"].runtimeClasspath
    shouldRunAfter("test")

    useJUnitPlatform()

    testLogging {
        events("passed")
    }
}

tasks.check { dependsOn(integrationTest) }
build.gradle
tasks.register('integrationTest', Test) {
    description = 'Runs integration tests.'
    group = 'verification'

    testClassesDirs = sourceSets.intTest.output.classesDirs
    classpath = sourceSets.intTest.runtimeClasspath
    shouldRunAfter test

    useJUnitPlatform()

    testLogging {
        events "passed"
    }
}

check.dependsOn integrationTest

同样,我们正在访问一个源代码集来获取相关信息,即编译后的测试类在哪里——testClassesDirs 属性——以及运行它们时类路径上需要什么——classpath

用户通常希望在单元测试之后运行集成测试,因为集成测试通常运行较慢,并且您希望构建在单元测试阶段尽早失败,而不是在集成测试阶段才失败。这就是上面示例添加 shouldRunAfter() 声明的原因。这比 mustRunAfter() 更可取,因为这样 Gradle 在并行执行构建时拥有更大的灵活性。

有关如何确定额外源代码集中的测试代码覆盖率的信息,请参阅 JaCoCo 插件JaCoCo 报告聚合插件 的章节。

测试 Java 模块

如果您正在开发 Java 模块,本章中描述的所有内容仍然适用,并且可以使用任何受支持的测试框架。但是,根据测试执行期间是否需要模块信息可用以及是否需要强制执行模块边界,有一些事项需要考虑。在这种情况下,经常使用术语 白盒测试(模块边界被停用或放宽)和 黑盒测试(模块边界被保留)。白盒测试用于/需要进行单元测试,而黑盒测试适合功能或集成测试的要求。

在类路径上执行白盒单元测试

为模块中的函数或类编写单元测试的最简单设置是在测试执行期间不使用模块特性。为此,您只需像为普通库编写测试一样编写测试。如果您的测试源代码集(src/test/java)中没有 module-info.java 文件,则该源代码集在编译和测试运行时将被视为传统的 Java 库。这意味着所有依赖项,包括带有模块信息的 Jar 包,都将放在类路径上。这样做的好处是,您的(或其他)模块的所有内部类都可以直接在测试中访问。这对于单元测试来说可能是一个完全有效的设置,我们不需要关心更大的模块结构,而只关心测试单个函数。

如果您使用 Eclipse:默认情况下,Eclipse 也使用模块补丁(module patching)(参见下文)将单元测试作为模块运行。在导入的 Gradle 项目中,使用 Eclipse 测试运行器对模块进行单元测试可能会失败。然后,您需要手动调整测试运行配置中的类路径/模块路径,或者将测试执行委托给 Gradle。

这仅涉及测试执行。单元测试的编译和开发在 Eclipse 中运行良好。

黑盒集成测试

对于集成测试,您可以选择将测试集本身定义为额外的模块。这类似于将主源文件转变为模块的方式:通过在相应的源代码集中添加一个 module-info.java 文件(例如 integrationTests/java/module-info.java)。

您可以在此处找到一个包含黑盒集成测试的完整示例。

在 Eclipse 中,目前不支持在一个项目中编译多个模块。因此,此处描述的集成测试(黑盒)设置仅在测试被移动到单独的子项目时才能在 Eclipse 中工作。

使用模块补丁执行白盒测试

另一种进行白盒测试的方法是留在模块世界中,通过将测试补丁到被测模块中。这样,模块边界保持不变,但测试本身成为被测模块的一部分,然后可以访问模块的内部。

这与哪些用例相关以及如何最好地实现这一点是一个讨论话题。目前没有通用的最佳方法。因此,Gradle 目前对此没有特殊支持。

但是,您可以像这样为测试设置模块补丁

  • 将一个 module-info.java 文件添加到您的测试源代码集中,该文件是主 module-info.java 文件的副本,并添加了测试所需的额外依赖项(例如 requires org.junit.jupiter.api)。

  • 配置 testCompileJavatest 任务,使用参数将被测主类与测试类进行补丁,如下所示。

build.gradle.kts
val moduleName = "org.gradle.sample"
val patchArgs = listOf("--patch-module", "$moduleName=${tasks.compileJava.get().destinationDirectory.asFile.get().path}")
tasks.compileTestJava {
    options.compilerArgs.addAll(patchArgs)
}
tasks.test {
    jvmArgs(patchArgs)
}
build.gradle
def moduleName = "org.gradle.sample"
def patchArgs = ["--patch-module", "$moduleName=${tasks.compileJava.destinationDirectory.asFile.get().path}"]
tasks.named('compileTestJava') {
    options.compilerArgs += patchArgs
}
tasks.named('test') {
    jvmArgs += patchArgs
}
如果使用自定义参数进行补丁,Eclipse 和 IDEA 将不会识别这些参数。您很可能会在 IDE 中看到无效的编译错误。

跳过测试

如果您想在运行构建时跳过测试,有几种选择。您可以通过命令行参数在构建脚本中实现。要在命令行上执行此操作,可以使用 -x--exclude-task 选项,如下所示

gradle build -x test

这会排除 test 任务以及它唯一依赖的任何其他任务,即没有其他任务依赖于同一任务。这些任务不会被 Gradle 标记为“SKIPPED”,而只会不出现在已执行任务列表中。

通过构建脚本跳过测试可以通过几种方式实现。一种常见的方法是通过 Task.onlyIf(String, org.gradle.api.specs.Spec) 方法使测试执行具有条件性。以下示例如果项目具有名为 mySkipTests 的属性,则会跳过 test 任务

build.gradle.kts
tasks.test {
    val skipTestsProvider = providers.gradleProperty("mySkipTests")
    onlyIf("mySkipTests property is not set") {
        !skipTestsProvider.isPresent()
    }
}
build.gradle
def skipTestsProvider = providers.gradleProperty('mySkipTests')
test.onlyIf("mySkipTests property is not set") {
    !skipTestsProvider.present
}

在这种情况下,Gradle 会将跳过的测试标记为“SKIPPED”,而不是将它们从构建中排除。

强制运行测试

在定义良好的构建中,您可以依赖 Gradle 仅在测试本身或生产代码发生更改时才运行测试。但是,您可能会遇到测试依赖于第三方服务或其他可能发生变化但在构建中无法建模的情况。

您始终可以使用 --rerun 内置任务选项来强制任务重新运行。

gradle test --rerun

或者,如果未启用构建缓存(build caching),您也可以通过清除相关 Test 任务(例如 test)的输出来强制运行测试,然后再次运行测试,如下所示

gradle cleanTest test

cleanTest 基于 Base 插件 提供的任务规则。您可以将它用于任何任务。

运行测试时进行调试

在您想要在测试运行时调试代码的少数情况下,如果此时能够附加调试器会很有帮助。您可以将 Test.getDebug() 属性设置为 true,或使用 --debug-jvm 命令行选项,或使用 --no-debug-jvm 将其设置为 false。

当测试的调试功能启用时,Gradle 将启动测试进程,使其暂停并监听端口 5005。

您也可以在 DSL 中启用调试,在那里还可以配置其他属性

test {
    debugOptions {
        enabled = true
        host = 'localhost'
        port = 4455
        server = true
        suspend = true
    }
}

通过此配置,测试 JVM 的行为将与传递 --debug-jvm 参数时相同,但它将在端口 4455 上监听。

要通过网络远程调试测试进程,需要将 host 设置为机器的 IP 地址或 "*"(监听所有接口)。

使用测试固件(test fixtures)

在单个项目中生成和使用测试固件

测试固件通常用于设置被测代码,或提供旨在简化组件测试的实用程序。Java 项目可以通过应用 java-test-fixtures 插件来启用测试固件支持,该插件需与 javajava-library 插件一起应用

lib/build.gradle.kts
plugins {
    // A Java Library
    `java-library`
    // which produces test fixtures
    `java-test-fixtures`
    // and is published
    `maven-publish`
}
lib/build.gradle
plugins {
    // A Java Library
    id 'java-library'
    // which produces test fixtures
    id 'java-test-fixtures'
    // and is published
    id 'maven-publish'
}

这将自动创建一个 testFixtures 源代码集,您可以在其中编写测试固件。测试固件的配置如下:

  • 它们可以看到 main 源代码集的类

  • test 源文件可以看到 test fixtures 的类

例如,对于这个主类

src/main/java/com/acme/Person.java
public class Person {
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    // ...

测试固件可以写在 src/testFixtures/java

src/testFixtures/java/com/acme/Simpsons.java
public class Simpsons {
    private static final Person HOMER = new Person("Homer", "Simpson");
    private static final Person MARGE = new Person("Marjorie", "Simpson");
    private static final Person BART = new Person("Bartholomew", "Simpson");
    private static final Person LISA = new Person("Elisabeth Marie", "Simpson");
    private static final Person MAGGIE = new Person("Margaret Eve", "Simpson");
    private static final List<Person> FAMILY = new ArrayList<Person>() {{
        add(HOMER);
        add(MARGE);
        add(BART);
        add(LISA);
        add(MAGGIE);
    }};

    public static Person homer() { return HOMER; }

    public static Person marge() { return MARGE; }

    public static Person bart() { return BART; }

    public static Person lisa() { return LISA; }

    public static Person maggie() { return MAGGIE; }

    // ...

声明测试固件的依赖项

Java 库插件 类似,测试固件暴露了一个 API 和一个实现配置

lib/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13")

    // API dependencies are visible to consumers when building
    testFixturesApi("org.apache.commons:commons-lang3:3.9")

    // Implementation dependencies are not leaked to consumers when building
    testFixturesImplementation("org.apache.commons:commons-text:1.6")
}
lib/build.gradle
dependencies {
    testImplementation 'junit:junit:4.13'

    // API dependencies are visible to consumers when building
    testFixturesApi 'org.apache.commons:commons-lang3:3.9'

    // Implementation dependencies are not leaked to consumers when building
    testFixturesImplementation 'org.apache.commons:commons-text:1.6'
}

值得注意的是,如果一个依赖项是测试固件的 implementation 依赖项,那么在编译依赖于这些测试固件的测试时,实现依赖项将不会泄露到编译类路径中。这带来了改进的关注点分离和更好的编译避免。

消费另一个项目的测试固件

测试固件不限于单个项目。通常情况下,依赖项目的测试也需要该依赖项的测试固件。这可以使用 testFixtures 关键字非常容易地实现

build.gradle.kts
dependencies {
    implementation(project(":lib"))

    testImplementation("junit:junit:4.13")
    testImplementation(testFixtures(project(":lib")))
}
build.gradle
dependencies {
    implementation(project(":lib"))

    testImplementation 'junit:junit:4.13'
    testImplementation(testFixtures(project(":lib")))
}

发布测试固件

使用 java-test-fixtures 插件的优点之一是测试固件会被发布。按照约定,测试固件将使用具有 test-fixtures 分类符(classifier)的制品(artifact)发布。对于 Maven 和 Ivy,具有该分类符的制品只是与常规制品一起发布。但是,如果您使用 maven-publishivy-publish 插件,测试固件将作为附加变体(variant)发布在 Gradle Module Metadata 中,您可以在另一个 Gradle 项目中直接依赖外部库的测试固件

build.gradle.kts
dependencies {
    // Adds a dependency on the test fixtures of Gson, however this
    // project doesn't publish such a thing
    functionalTest(testFixtures("com.google.code.gson:gson:2.8.5"))
}
build.gradle
dependencies {
    // Adds a dependency on the test fixtures of Gson, however this
    // project doesn't publish such a thing
    functionalTest testFixtures("com.google.code.gson:gson:2.8.5")
}

值得注意的是,如果外部项目未发布 Gradle Module Metadata,则解析将失败,并显示指示找不到此类变体(variant)的错误

gradle dependencyInsight --configuration functionalTestClasspath --dependency gson 的输出
> gradle dependencyInsight --configuration functionalTestClasspath --dependency gson

> Task :dependencyInsight
com.google.code.gson:gson:2.8.5 FAILED
   Failures:
      - Could not resolve com.google.code.gson:gson:2.8.5.
          - Unable to find a variant with the requested capability: feature 'test-fixtures':
               - Variant 'compile' provides 'com.google.code.gson:gson:2.8.5'
               - Variant 'enforced-platform-compile' provides 'com.google.code.gson:gson-derived-enforced-platform:2.8.5'
               - Variant 'enforced-platform-runtime' provides 'com.google.code.gson:gson-derived-enforced-platform:2.8.5'
               - Variant 'javadoc' provides 'com.google.code.gson:gson:2.8.5'
               - Variant 'platform-compile' provides 'com.google.code.gson:gson-derived-platform:2.8.5'
               - Variant 'platform-runtime' provides 'com.google.code.gson:gson-derived-platform:2.8.5'
               - Variant 'runtime' provides 'com.google.code.gson:gson:2.8.5'
               - Variant 'sources' provides 'com.google.code.gson:gson:2.8.5'

com.google.code.gson:gson:2.8.5 FAILED
\--- functionalTestClasspath

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

错误消息提到了缺失的 com.google.code.gson:gson-test-fixtures 能力(capability),该能力确实未在此库中定义。这是因为根据约定,对于使用 java-test-fixtures 插件的项目,Gradle 会自动创建测试固件变体(variant),其能力名称为主组件的名称,并带有 -test-fixtures 后缀。

如果您发布了您的库并使用了测试固件,但不想发布这些固件,可以按如下所示停用测试固件变体(variant)的发布。
build.gradle.kts
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() }
build.gradle
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }

1. JUnit wiki 详细描述了如何使用 JUnit categories:https://github.com/junit-team/junit/wiki/Categories
2. TestNG 文档包含了关于测试组(test groups)的更多详细信息:https://testng.org/#_test_groups
3. TestNG 文档包含了在使用 testng.xml 文件时关于测试顺序的更多详细信息:http://testng.org/doc/documentation-main.html#testng-xml