JVM 测试套件插件(插件 ID:jvm-test-suite)提供了一个 DSL 和 API,用于在基于 JVM 的项目中将多组自动化测试建模为测试套件。测试套件旨在按其目的进行分组,并且可以具有单独的依赖关系并使用不同的测试框架。

例如,此插件可用于定义一组集成测试,这些测试的运行时间可能比单元测试长得多,并且具有不同的环境要求。

JVM 测试套件插件是一个 孵化中 的 API,并且在未来的版本中可能会发生更改。

用法

此插件由 java 插件自动应用,但如果需要,也可以额外显式应用。如果没有应用 JVM 语言插件,则无法使用该插件,因为它依赖于 java 插件的多个约定。

build.gradle.kts
plugins {
    java
    `jvm-test-suite`
}
build.gradle
plugins {
    id 'java'
    id 'jvm-test-suite'
}

插件将以下对象添加到项目中

  • 一个 testing 扩展(类型:TestingExtension),用于配置测试套件。

当与 Java 插件 一起使用时

  • 一个名为 test 的测试套件(类型:JvmTestSuite)。

  • 一个 test SourceSet

  • test SourceSet 名称派生的多个配置:testImplementationtestCompileOnlytestRuntimeOnly

  • 一个由名为 test 的 task 支持的单个测试套件目标。

test task、SourceSet 和派生配置在名称和功能上与之前 Gradle 版本中使用的那些相同。

Task

JVM 测试套件插件将以下 task 添加到项目中

testTest

依赖于:来自 java 插件的 testClasses,以及所有生成测试运行时类路径的 task

使用为默认测试套件配置的框架运行测试。

将为通过 testing 扩展添加的每个测试套件自动创建 Test task 的其他实例。

配置

请参阅 API 文档中的 TestingExtension 类。

术语和建模

JVM 测试套件插件引入了一些由新 API 支持的建模概念。以下是它们的定义。

测试套件

测试套件是基于 JVM 的测试的集合。

测试套件目标

对于此插件的初始版本,每个测试套件都有一个目标。 这导致测试套件、测试套件目标和匹配的 Test task 之间存在 1:1:1 的关系。Test task 的名称派生自套件名称。该插件的未来迭代将允许基于其他属性(例如特定的 JDK/JRE 运行时)定义多个目标。

每个测试套件都有一些配置,这些配置对于套件中包含的所有测试都是通用的

  • 测试框架

  • 依赖

将来,可能会在测试套件中指定其他属性,这些属性可能会影响选择用于编译和运行测试的工具链。

与目标关联的 Test task 从套件继承其名称。 Test task 的其他属性是可配置的。

配置示例

以下是一些示例,用于说明测试套件的可配置性。

声明额外的测试套件

build.gradle.kts
testing {
    suites { (1)
        val test by getting(JvmTestSuite::class) { (2)
            useJUnitJupiter() (3)
        }

        register<JvmTestSuite>("integrationTest") { (4)
            dependencies {
                implementation(project()) (5)
            }

            targets { (6)
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                    }
                }
            }
        }
    }
}

tasks.named("check") { (7)
    dependsOn(testing.suites.named("integrationTest"))
}
build.gradle
testing {
    suites { (1)
        test { (2)
            useJUnitJupiter() (3)
        }

        integrationTest(JvmTestSuite) { (4)
            dependencies {
                implementation project() (5)
            }

            targets { (6)
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                    }
                }
            }
        }
    }
}

tasks.named('check') { (7)
    dependsOn(testing.suites.integrationTest)
}
1 为此项目配置所有测试套件。
2 配置内置的 test 套件。此套件是为向后兼容性而自动创建的。你必须指定要使用的测试框架才能运行这些测试(例如,JUnit 4、JUnit Jupiter)。此套件是唯一可以自动访问生产源的 implementation 依赖项的套件,所有其他套件都必须显式声明这些依赖项。
3 声明此测试套件使用 JUnit Jupiter。框架的依赖项会自动包含在内。即使需要 JUnit4,也始终需要显式配置内置的 test 套件。
4 定义一个名为 integrationTest 的新套件。请注意,除了内置的 test 套件之外的所有其他套件,按照约定,都将像调用了 useJUnitJupiter() 一样工作。除非你希望更改为另一个框架,否则不必在这些额外的套件上显式配置测试框架。
5 将项目生产代码的依赖项添加到 integrationTest 套件目标。按照约定,只有内置的 test 套件会自动依赖于项目的生产代码。
6 配置此套件的所有目标。按照约定,测试套件目标与其他 Test task 没有关系。此示例表明,较慢的集成测试套件目标应在 test 套件的所有目标完成后运行。
7 配置 check task 以依赖于所有 integrationTest 目标。按照约定,测试套件目标与 check task 不关联。

在上述配置的构建上调用 check task 应显示类似于以下的输出

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :compileIntegrationTestJava
> Task :processIntegrationTestResources NO-SOURCE
> Task :integrationTestClasses
> Task :integrationTest
> Task :check

BUILD SUCCESSFUL in 0s
6 actionable tasks: 6 executed

请注意,integrationTest 测试套件在 test 测试套件完成后才会运行。

配置内置的 test 套件

build.gradle.kts
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useTestNG() (1)

            targets {
                all {
                    testTask.configure { (2)
                        // set a system property for the test JVM(s)
                        systemProperty("some.prop", "value")
                        options { (3)
                            val options = this as TestNGOptions
                            options.preserveOrder = true
                        }
                    }
                }
            }
        }
    }
}
build.gradle
testing { (1)
    suites {
        test {
            useTestNG() (1)

            targets {
                all {
                    testTask.configure { (2)
                        // set a system property for the test JVM(s)
                        systemProperty 'some.prop', 'value'
                        options { (3)
                            preserveOrder = true
                        }
                    }
                }
            }
        }
    }
}
1 声明 test 测试套件使用 TestNG 测试框架。
2 延迟配置套件所有目标的测试 task;请注意 testTask 的返回类型是 TaskProvider<Test>
3 配置更详细的测试框架选项。 options 将是 org.gradle.api.tasks.testing.TestFrameworkOptions 的子类,在本例中它是 org.gradle.api.tasks.testing.testng.TestNGOptions

配置测试套件的依赖

build.gradle.kts
testing {
    suites {
        val test by getting(JvmTestSuite::class) { (1)
            dependencies {
                // Note that this is equivalent to adding dependencies to testImplementation in the top-level dependencies block
                implementation("org.assertj:assertj-core:3.21.0") (2)
                annotationProcessor("com.google.auto.value:auto-value:1.9") (3)
            }
        }
    }
}
build.gradle
testing {
    suites {
        test { (1)
            dependencies {
                // Note that this is equivalent to adding dependencies to testImplementation in the top-level dependencies block
                implementation 'org.assertj:assertj-core:3.21.0' (2)
                annotationProcessor 'com.google.auto.value:auto-value:1.9' (3)
            }
        }
    }
}
1 配置内置的 test 测试套件。
2 将 assertj 库添加到测试的编译和运行时类路径。测试套件中的 dependencies 代码块已经限定在该测试套件的范围内。无需知道配置的全局名称,测试套件具有一致的名称,你可以在此代码块中使用该名称来声明 implementationcompileOnlyruntimeOnlyannotationProcessor 依赖项。
3 将 Auto Value 注解处理器添加到套件的注解处理器类路径,以便在编译测试时运行它。

配置测试套件的依赖以引用项目输出

build.gradle.kts
dependencies {
    api("com.google.guava:guava:30.1.1-jre") (1)
    implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3") (2)
}

testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(project()) (3)
            }
        }
    }
}
build.gradle
dependencies {
    api 'com.google.guava:guava:30.1.1-jre' (1)
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' (2)
}

testing {
    suites {
        integrationTest(JvmTestSuite) {
            dependencies {
                implementation project() (3)
            }
        }
    }
}
1 添加一个生产依赖项,该依赖项用作库公共 API 的一部分。
2 添加一个仅在内部使用且未作为此项目公共类一部分公开的生产依赖项。
3 配置一个新的测试套件,该套件将 project() 依赖项添加到套件的编译和运行时类路径。此依赖项提供对项目输出以及在其 apicompileOnlyApi 配置中声明的任何依赖项的访问。

配置测试套件的源目录

build.gradle.kts
testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) { (1)
            sources { (2)
                java { (3)
                    setSrcDirs(listOf("src/it/java")) (4)
                }
            }
        }
    }
}
build.gradle
testing {
    suites {
        integrationTest(JvmTestSuite) { (1)
            sources { (2)
                java { (3)
                    srcDirs = ['src/it/java'] (4)
                }
            }
        }
    }
}
1 声明和配置一个名为 integrationTest 的套件。 SourceSet 和合成的 Test task 将基于此名称。
2 配置测试套件的 sources
3 配置测试套件的 java SourceDirectorySet。
4 覆盖 srcDirs 属性,将传统的 src/integrationTest/java 位置替换为 src/it/java

为测试套件配置 Test task

build.gradle.kts
testing {
    suites {
        val integrationTest by getting(JvmTestSuite::class) {
            targets {
                all { (1)
                    testTask.configure {
                        setMaxHeapSize("512m") (2)
                    }
                }
            }
        }
    }
}
build.gradle
testing {
    suites {
        integrationTest {
            targets {
                all { (1)
                    testTask.configure {
                        maxHeapSize = '512m' (2)
                    }
                }
            }
        }
    }
}
1 配置通过声明同名套件创建的 integrationTest task。
2 配置 Test task 属性。

与测试套件目标关联的 Test task 也可以直接通过名称进行配置。无需通过测试套件 DSL 进行配置。

在多个测试套件之间共享配置

有几种方法可以在多个测试套件之间共享配置,以避免重复依赖项或其他配置。每种方法都是灵活性与声明式与命令式配置样式之间的权衡。

  1. 使用 suites 容器上的 configureEach 方法以相同的方式配置每个测试套件。

  2. withTypematchingconfigureEach 一起使用,以过滤测试套件并配置其中的子集。

  3. 将配置代码块提取到局部变量,并仅将其应用于所需的测试套件。

方法 1:使用 configureEach

这是在每个测试套件之间共享配置的最直接方法。配置将应用于每个测试套件。

build.gradle.kts
testing {
    suites {
        withType<JvmTestSuite> { (1)
            useJUnitJupiter()
            dependencies { (2)
                implementation("org.mockito:mockito-junit-jupiter:4.6.1")
            }
        }
        (3)
        val integrationTest by registering(JvmTestSuite::class)
        val functionalTest by registering(JvmTestSuite::class) {
            dependencies { (4)
                implementation("org.apache.commons:commons-lang3:3.11")
            }
        }
    }
}
build.gradle
testing {
    suites {
        configureEach { (1)
            useJUnitJupiter()
            dependencies { (2)
                implementation('org.mockito:mockito-junit-jupiter:4.6.1')
            }
        }
        (3)
        integrationTest(JvmTestSuite)
        functionalTest(JvmTestSuite) {
            dependencies { (4)
                implementation('org.apache.commons:commons-lang3:3.11')
            }
        }
    }
}
1 配置每个 JVM 测试套件
2 提供所有测试套件共享的依赖项
3 定义将在创建时配置的额外测试套件
4 添加特定于 functionalTest 测试套件的依赖项

方法 2:使用 withTypematchingconfigureEach

此方法添加了过滤命令,仅将配置应用于测试套件的子集,如名称所标识。

build.gradle.kts
testing {
    suites {
        withType(JvmTestSuite::class).matching { it.name in listOf("test", "integrationTest") }.configureEach { (1)
            useJUnitJupiter()
            dependencies {
                implementation("org.mockito:mockito-junit-jupiter:4.6.1")
            }
        }
        val integrationTest by registering(JvmTestSuite::class)
        val functionalTest by registering(JvmTestSuite::class) {
            useJUnit() (2)
            dependencies { (3)
                implementation("org.apache.commons:commons-lang3:3.11")
            }
        }
    }
}
build.gradle
testing {
    suites {
        withType(JvmTestSuite).matching { it.name in ['test', 'integrationTest'] }.configureEach { (1)
            useJUnitJupiter()
            dependencies {
                implementation('org.mockito:mockito-junit-jupiter:4.6.1')
            }
        }

        integrationTest(JvmTestSuite)
        functionalTest(JvmTestSuite) {
            useJUnit() (2)
            dependencies { (3)
                implementation('org.apache.commons:commons-lang3:3.11')
            }
        }
    }
}
1 配置每个符合给定条件的 JVM 测试套件
2 functionalTest 测试套件使用不同的测试框架
3 添加特定于 functionalTest 测试套件的依赖项

方法 3:提取自定义配置代码块

此方法是最灵活的,但也是最命令式的。

build.gradle.kts
testing {
    suites {
        val applyMockito = { suite: JvmTestSuite -> (1)
            suite.useJUnitJupiter()
            suite.dependencies {
                implementation("org.mockito:mockito-junit-jupiter:4.6.1")
            }
        }

        /* This is the equivalent of:
            val test by getting(JvmTestSuite::class) {
                applyMockito(this)
            }
         */
        val test by getting(JvmTestSuite::class, applyMockito)  (2)

        /* This is the equivalent of:
            val integrationTest by registering(JvmTestSuite::class)
            applyMockito(integrationTest.get())
         */
        val integrationTest by registering(JvmTestSuite::class, applyMockito) (3)

        val functionalTest by registering(JvmTestSuite::class) {
            useJUnit()
            dependencies {
                implementation("org.apache.commons:commons-lang3:3.11")
            }
        }
    }
}
build.gradle
testing {
    suites {
        def applyMockito = { suite -> (1)
            suite.useJUnitJupiter()
            suite.dependencies {
                implementation('org.mockito:mockito-junit-jupiter:4.6.1')
            }
        }

        /* This is the equivalent of:
            test {
                applyMockito(this)
            }
         */
        test(applyMockito) (2)

        /* This is the equivalent of:
            integrationTest(JvmTestSuite)
            applyMockito(integrationTest)
         */
        integrationTest(JvmTestSuite, applyMockito) (3)

        functionalTest(JvmTestSuite) {
            useJUnit()
            dependencies {
                implementation('org.apache.commons:commons-lang3:3.11')
            }
        }
    }
}
1 定义一个闭包,该闭包接受单个 JvmTestSuite 参数并对其进行配置
2 将闭包应用于测试套件,使用默认的 (test) 测试套件
3 在测试套件声明之外,使用 integrationTest 测试套件,应用配置闭包的替代方法

JvmTestSuiteTest task 类型上相似方法之间的差异

JvmTestSuite 的实例具有方法 useJUnit()useJUnitJupiter(),这些方法在名称上与 Test task 类型上的方法类似:useJUnit()useJUnitPlatform()。但是,存在重要的差异。

Test task 上的方法不同,JvmTestSuite 方法执行两个额外的配置

  1. JvmTestSuite#useJUnit()#useJUnitJupiter() 和其他特定于框架的方法会自动将相关的测试框架库应用于套件目标的编译和运行时类路径。 请注意,重载方法(如 JvmTestSuite#useJUnit(String)#useJUnitJupiter(String))允许你提供框架依赖项的特定版本。

  2. JvmTestSuite#useJUnit()#useJUnitJupiter() 会自动配置套件目标的 Test task,以使用指定的框架执行。

上面的第一个配置示例 配置内置的 test 套件 中显示了此行为的示例。

Test task 不同,JvmTestSuite 上的上述方法不接受用于配置框架的闭包或操作。这些框架配置选项可以在各个目标上设置。

传出变体

每个测试套件都会创建一个传出变体,其中包含其测试执行结果。这些变体旨在供 Test Report Aggregation Plugin 使用。

属性将类似于以下内容。用户可配置的属性在示例下方突出显示。

outgoingVariants task 输出
--------------------------------------------------
Variant testResultsElementsForTest (i)
--------------------------------------------------
Description = Directory containing binary results of running tests for the test Test Suite's test target.

Capabilities
    - org.gradle.sample:list:1.0.2 (default capability)
Attributes
    - org.gradle.category         = verification
    - org.gradle.testsuite.name   = test           (1)
    - org.gradle.verificationtype = test-results

Artifacts
    - build/test-results/test/binary (artifactType = directory)
1 TestSuiteName 属性;值派生自 TestSuite#getName()