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

它解释说:

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

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

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

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

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

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

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

基础

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

为了进行操作,Test任务类型只需要两条信息:

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

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

  • 运行这些单元测试的任务test类型Test

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

测试任务有许多通用配置选项以及几个特定于框架的选项,您可以在JUnitOptionsJUnitPlatformOptionsTestNGOptions中找到这些选项的描述。我们将在本章的其余部分介绍其中的大量内容。

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

测试执行

Gradle 在单独的(“分叉的”)JVM 中执行测试,与主构建过程隔离。这可以防止构建过程中的类路径污染和过多的内存消耗。它还允许您使用与构建所使用的不同的 JVM 参数来运行测试。

您可以通过任务的多个属性来控制测试流程的启动方式Test,包括以下属性:

maxParallelForks— 默认:1

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

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

forkEvery— 默认值:0(无最大值)

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

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

ignoreFailures— 默认值:假

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

failFast—(自 Gradle 4.6 起)默认值: false

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

您还可以使用--fail-fast命令行选项启用此行为,或使用 分别禁用它--no-fail-fast

testLogging— 默认值:未设置

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

dryRun— 默认值:假

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

您还可以使用--test-dry-run命令行选项启用此行为,或使用 分别禁用它--no-test-dry-run

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

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

例如,如果SecurityManager在测试中修改 a ,则可能会出现问题,因为 Gradle 的内部消息传递依赖于反射和套接字通信,如果安全管理器上的权限发生更改,则可能会中断。在这种特殊情况下,您应该SecurityManager在测试后恢复原始状态,以便gradle测试工作进程可以继续运行。

测试过滤

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

  • 过滤(首选)

  • 测试包含/排除

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

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

  • 完全限定的类名或完全限定的方法名称,例如org.gradle.SomeTestorg.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.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 服务器和其他工具

测试任务按照“JUnit XML”伪标准创建描述测试结果的 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")
    }
}

通过上述配置,任务的 XML 文件test将被写入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选项后,会将测试用例期间生成的任何输出日志记录与结果中的该测试用例关联起来。当禁用(默认)时,输出与整个测试类相关联,而不是与生成日志输出的各个测试用例(例如测试方法)相关联。大多数观察 JUnit XML 文件的现代工具都支持“每个测试用例的输出”格式。

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

合并重播

启用后mergeReruns,如果测试失败但随后重试并成功,则其失败将被记录为,<flakyFailure>而不是<failure>, 在 1 内<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 包含测试类,它们也将运行。

如果您不想使用测试类检测,可以通过将TestscanForTestClasses上的属性设置为来禁用它。当您这样做时,测试任务仅使用和属性来查找测试类。falseincludesexcludes

如果scanForTestClasses为 false 并且未指定包含或排除模式,Gradle 默认运行与模式**/*Tests.class和匹配的任何类**/*Test.class,但不包括与 匹配的类**/Abstract*.class

对于JUnit Platform,仅includesexcludes用于过滤测试类 —scanForTestClasses没有效果。

测试记录

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

当日志级别为

记录的事件

附加配置

ERRORQUIET或者WARNING

没有任何

没有任何

LIFECYCLE

测试失败

异常格式为SHORT

INFO

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

堆栈跟踪被截断。

DEBUG

所有活动

记录完整的堆栈跟踪。

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

测试分组

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

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

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

Example 8. JUnit Categories
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 的测试。

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

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 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/AfterClassand注释的方法)。@Before/AfterMethod您可以通过将TestNGOptions.getPreserveOrder()属性设置为 来执行此操作true。如果设置为false,则可能会遇到执行顺序类似于:TestA.doBeforeClass()TestB.doBeforeClass()TestA测试的场景。

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

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

groupByInstance属性控制测试是否应按实例而不是按类分组。TestNG 文档更详细地解释了差异,但本质上,如果您有一个A()依赖于 的测试方法B(),则按实例分组可确保每个 AB 配对(例如B(1)- A(1))在下一个配对之前执行。对于按类分组,B()先运行所有方法,然后运行所有A()方法。

请注意,如果您使用数据提供程序对其进行参数化,则通常只有多个测试实例。此外,TestNG 版本 6.1 还引入了按实例对测试进行分组的功能。将groupByInstances属性设置true为较旧的 TestNG 版本将导致构建失败。

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配置(以及其他一些不太常用的配置)

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

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

该示例还执行以下操作,并非您的特定集成测试可能需要的所有操作:

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

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

  • intTestRuntimeOnly对配置执行同样的操作

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

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

您应该注意该示例的其他几个方面:

  • +=允许您附加路径和路径集合compileClasspath,而runtimeClasspath不是覆盖它们

  • 如果要使用基于约定的配置,例如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 模块,本章中描述的所有内容仍然适用,并且可以使用任何受支持的测试框架。但是,根据您是否需要在测试执行期间提供可用的模块信息以及是否需要强制执行模块边界,需要考虑一些事项。在这种情况下,经常使用术语白盒测试(模块边界被停用或放宽)和黑盒测试(模块边界到位)。白盒测试用于/需要单元测试,黑盒测试适合功能或集成测试要求。

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

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

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

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

黑盒集成测试

对于集成测试,您可以选择将测试集本身定义为附加模块。您执行此操作的方式与将主要源代码转换为模块的方式类似:通过将module-info.java文件添加到相应的源代码集(例如integrationTests/java/module-info.java)。

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

在Eclipse中,目前不支持 在一个项目中编译多个模块。因此,如果测试移至单独的子项目,则此处描述的集成测试(黑盒)设置仅适用于 Eclipse。

通过模块修补执行白盒测试

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

这对于哪些用例相关以及如何最好地完成是讨论的主题。目前还没有通用的最佳方法。因此,Gradle 目前没有对此提供特殊支持。

但是,您可以为测试设置模块修补,如下所示:

  • 将 a 添加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)方法使测试执行有条件。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 会将跳过的测试标记为“SKIPPED”,而不是将它们从构建中排除。

强制测试运行

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

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

gradle test --rerun

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

gradle cleanTest test

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

运行测试时进行调试

在少数情况下,您想要在测试运行时调试代码,如果此时可以附加调试器,将会很有帮助。您可以将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地址或"*"(监听所有接口)。

使用测试夹具

在单个项目中生产和使用测试夹具

测试装置通常用于设置被测代码,或提供旨在促进组件测试的实用程序。java-test-fixtures除了 或java插件之外,Java 项目还可以通过应用插件来启用测试装置支持java-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源集,您可以在其中编写测试装置。测试夹具的配置使得:

  • 他们可以看到主要的源集类

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

例如对于这个主类:

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 Library Plugin类似,测试装置公开 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-publish或插件,测试装置将作为Gradle 模块元数据ivy-publish中的附加变体发布,并且您可以直接依赖另一个 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