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

它解释了

  • 控制测试运行方式的方法(测试执行

  • 如何选择要运行的特定测试(测试过滤

  • 生成哪些测试报告以及如何影响该过程(测试报告

  • Gradle 如何查找要运行的测试 (测试检测)

  • 如何利用主要框架的机制将测试组合在一起 (测试分组)

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

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

基础知识

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

为了运行,测试 任务类型只需要两条信息

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

  • 一个专用的 test 源集,用于单元测试

  • 一个类型为 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 在一个单独的(“分叉的”)JVM 中执行测试,该 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 测试工作进程可以继续运行。

测试筛选

运行测试套件的子集是一种常见需求,例如在修复错误或开发新测试用例时。Gradle 提供了两种机制来执行此操作

  • 筛选(首选选项)

  • 测试包含/排除

过滤取代包含/排除机制,但您仍可能在野外遇到后者。

使用 Gradle 的测试过滤,您可以根据以下内容选择要运行的测试

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

  • 如果模式以大写字母开头,则为简单的类名或方法名,例如 SomeTestSomeTest.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.baseDir = file("my-reports")
java.testResultsDir = layout.buildDirectory.dir("my-test-results")

tasks.register("showDirs") {
    val rootDir = project.rootDir
    val reportsDir = project.reporting.baseDirectory
    val testResultsDir = project.java.testResultsDir

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

tasks.register('showDirs') {
    def rootDir = project.rootDir
    def reportsDir = project.reporting.baseDirectory
    def testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(rootDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(rootDir.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 的变体感知依赖管理引擎公开项目中的测试结果。

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

你应该注意,TestReport 类型组合了多个测试任务的结果,并且需要聚合各个测试类别的结果。这意味着如果一个给定的测试类别由多个测试任务执行,那么测试报告将包括该类别的执行,但是很难区分该类别的各个执行及其输出。

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

测试任务创建 XML 文件来描述测试结果,采用“JUnit XML”伪标准。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 {
            isOutputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
build.gradle
test {
    reports {
        junitXml {
            outputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
outputPerTestCase

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

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

mergeReruns

当启用 mergeReruns 时,如果某个测试失败,但随后重试并成功,则其失败将记录为 <flakyFailure> 而不是 <failure>,在一个 <testcase> 内。这实际上是 Apache Maven™ 的 surefire 插件 在启用重试时产生的报告。如果您的 CI 服务器理解此格式,它将表明该测试不稳定。如果它不理解,它将表明测试成功,因为它将忽略 <flakyFailure> 信息。如果测试未成功(即它每次重试都失败),则无论您的工具是否理解此格式,都将表明它已失败。

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

如果您使用 构建扫描Develocity,无论此设置如何,都会检测到不稳定的测试。

当使用 CI 工具(该工具使用 XML 测试结果来确定构建失败,而不是依赖 Gradle 来确定构建是否失败)并且您希望在重试时所有失败的测试都通过时不认为构建失败时,启用此选项特别有用。Jenkins CI 服务器及其 JUnit 插件 的情况就是这样。启用 mergeReruns 后,重试通过的测试将不再导致此 Jenkins 插件认为构建已失败。但是,失败的测试执行将从 Jenkins 测试结果可视化中省略,因为它不考虑 <flakyFailure> 信息。除了 JUnit Jenkins 插件外,还可以使用单独的 Flaky Test Handler Jenkins 插件 来可视化此类“不稳定失败”。

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

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

测试检测

默认情况下,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 允许对记录到控制台的事件进行微调控制。日志记录可以按每个日志级别进行配置,默认情况下,记录以下事件

当日志级别为

记录的事件

其他配置

ERRORQUIETWARNING

LIFECYCLE

测试失败

异常格式为 SHORT

INFO

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

堆栈跟踪被截断。

DEBUG

所有事件

记录完整的堆栈跟踪。

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

测试分组

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

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

JUnit 4.8 引入了类别概念,用于对 JUnit 4 测试类和方法进行分组。[1] Test.useJUnit(org.gradle/api/Action) 允许您指定要包含和排除的 JUnit 类别。例如,以下配置包含 CategoryA 中的测试,并排除了 test 任务中 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 引入了 标记 来替换类别。您可以通过 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 框架使用测试组概念来实现类似的效果。[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 平台用作在 JVM 上启动测试框架的基础。JUnit Jupiter 是用于编写 JUnit 5 中的测试和扩展的新 编程模型扩展模型 的组合。JUnit Vintage 提供了一个 TestEngine,用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。

以下代码在 build.gradle 中启用 JUnit 平台支持

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 平台上运行 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 平台上测试 JUnit 3/4 测试,而无需重写它们。

过滤测试引擎

JUnit 平台允许您使用不同的测试引擎。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 文档 详细说明了差异,但从本质上讲,如果你有一个依赖于 B() 的测试方法 A(),按实例分组可确保在下一对之前执行每个 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) 进行报告。这使得轻松识别特定迭代的参数值变得容易。

配置集成测试

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

将集成测试添加到构建的最简单方法是利用孵化 JVM 测试套件 插件。如果孵化解决方案不适合你,以下是你需要在构建中执行的步骤

  1. 为它们创建一个新的 源集

  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 还使用模块修补程序将单元测试作为模块运行(请参见下文)。在导入的 Gradle 项目中,使用 Eclipse 测试运行器对模块进行单元测试可能会失败。然后,您需要在测试运行配置中手动调整类路径/模块路径,或将测试执行委托给 Gradle。

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

黑盒集成测试

对于集成测试,您可以选择将测试集本身定义为附加模块。您执行此操作的方式类似于将主源转换为模块的方式:向相应源集(例如 integrationTests/java/module-info.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 不会将这些任务标记为“已跳过”,但它们将不会出现在已执行任务的列表中。

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

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 会将已跳过的测试标记为“已跳过”,而不是从构建中排除它们。

强制运行测试

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

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

gradle test --rerun

或者,如果 构建缓存 未启用,你也可以通过清理相关 Test 任务(比如 test)的输出,然后重新运行测试来强制运行测试,如下所示

gradle cleanTest test

cleanTest 基于 基础插件 提供的任务规则。你可以将其用于任何任务。

运行测试时的调试

在少数情况下,当测试正在运行时,你希望调试你的代码,如果你可以在那时附加一个调试器,会很有帮助。你可以将 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 地址或 "*"(在所有接口上监听)。

使用测试固定装置

在单个项目中生成和使用测试固定装置

测试固定装置通常用于设置待测代码,或提供旨在促进组件测试的实用程序。除了 javajava-library 插件外,Java 项目可以通过应用 java-test-fixtures 插件来启用测试固定装置支持

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 源集,你可以在其中编写测试固定装置。测试固定装置的配置如下

  • 它们可以看到源集类

  • 测试源可以看到测试固定装置

例如,对于此主类

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'
}

值得注意的是,如果某个依赖项是测试夹具的实现依赖项,那么在编译依赖于这些测试夹具的测试时,实现依赖项不会泄漏到编译类路径中。这会导致关注点分离得更好,并且可以更好地避免编译。

使用另一个项目的测试夹具

测试夹具不限于单个项目。通常情况下,依赖项目的测试还需要依赖项的测试夹具。可以使用 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 分类器的工件。对于 Maven 和 Ivy,具有该分类器的工件只是与常规工件一起发布。但是,如果你使用 maven-publishivy-publish 插件,测试夹具将作为 Gradle 模块元数据 中的附加变体发布,你可以在另一个 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 模块元数据,那么解析将失败,并出现错误,指出找不到此类变体

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 of com.google.code.gson:gson:2.8.5 providing the requested capability com.google.code.gson:gson-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 功能,该功能实际上未为此库定义。这是因为根据惯例,对于使用 java-test-fixtures 插件的项目,Gradle 会自动创建带有功能的测试夹具变体,该功能的名称是主组件的名称,后缀为 -test-fixtures

如果你发布库并使用测试夹具,但不想发布夹具,则可以按如下所示停用测试夹具变体的发布。
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 类别的详细说明:https://github.com/junit-team/junit/wiki/Categories
2. TestNG 文档包含有关测试组的更多详细信息:http://testng.org/doc/documentation-main.html#test-groups
3. TestNG 文档包含在使用 testng.xml 文件时有关测试排序的更多详细信息:http://testng.org/doc/documentation-main.html#testng-xml