在 Java 和 & JVM 项目中进行测试
在 JVM 上进行测试是一个内容丰富的领域。有许多不同的测试库和框架,以及许多不同类型的测试。所有这些都需要成为构建的一部分,无论它们是频繁执行还是不频繁执行。本章致力于解释 Gradle 如何处理构建之间和构建内部的不同需求,并重点介绍它如何与两个最常见的测试框架集成:JUnit 和 TestNG。
本章解释了
但首先,让我们看看 Gradle 中 JVM 测试的基础知识。
通过孵化中的 JVM Test Suite 插件,可以使用新的配置 DSL 来建模测试执行阶段。 |
基础知识
所有 JVM 测试都围绕一个单一的 task 类型:Test。它使用任何受支持的测试库(JUnit、JUnit Platform 或 TestNG)运行测试用例集合,并整理结果。然后,您可以通过 TestReport task 类型的实例将这些结果转换为报告。
为了运行,Test
task 类型只需要两个信息
-
在哪里找到已编译的测试类(属性:Test.getTestClassesDirs())
-
执行 classpath,它应该包括被测试的类以及您正在使用的测试库(属性:Test.getClasspath())
当您使用 JVM 语言插件(例如 Java Plugin)时,您将自动获得以下内容
-
用于单元测试的专用
test
source set -
一个
test
task,类型为Test
,用于运行这些单元测试
JVM 语言插件使用 source set 配置 task,使其具有适当的执行 classpath 和包含已编译测试类的目录。此外,它们还将 test
task 附加到 check
生命周期 task。
还值得注意的是,test
source set 会自动创建 相应的依赖配置 — 其中最有用的是 testImplementation
和 testRuntimeOnly
— 插件将其绑定到 test
task 的 classpath。
在大多数情况下,您需要做的就是配置适当的编译和运行时依赖项,并将任何必要的配置添加到 test
task。以下示例显示了一个简单的设置,该设置使用 JUnit Platform 并将测试 JVM 的最大堆大小更改为 1 GB
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")
}
}
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 task 具有许多通用配置选项以及几个特定于框架的选项,您可以在 JUnitOptions、JUnitPlatformOptions 和 TestNGOptions 中找到描述。我们在本章的其余部分中介绍了其中的许多选项。
如果您想使用自己的测试类集合设置自己的 Test
task,那么最简单的方法是创建自己的 source set 和 Test
task 实例,如 配置集成测试 中所示。
测试执行
Gradle 在与主构建过程隔离的单独(“forked”)JVM 中执行测试。这可以防止 classpath 污染和构建过程的过度内存消耗。它还允许您使用与构建不同的 JVM 参数运行测试。
您可以通过 Test
task 上的多个属性来控制测试过程的启动方式,包括以下属性
maxParallelForks
— 默认值:1-
您可以通过将此属性设置为大于 1 的值来并行运行测试。这可能会使您的测试套件更快完成,尤其是在多核 CPU 上运行时。当使用并行测试执行时,请确保您的测试彼此正确隔离。与文件系统交互的测试尤其容易发生冲突,从而导致间歇性测试失败。
您的测试可以通过使用
org.gradle.test.worker
属性的值来区分并行测试进程,该属性对于每个进程都是唯一的。您可以将其用于任何您想要的内容,但它对于文件名和其他资源标识符特别有用,以防止我们刚才提到的那种冲突。 forkEvery
— 默认值:0(无最大值)-
此属性指定 Gradle 在处理并创建新进程之前,应在测试进程上运行的最大测试类数。这主要用作管理泄漏测试或具有静态状态的框架的一种方法,这些静态状态无法在测试之间清除或重置。
警告:较低的值(0 除外)会严重损害测试的性能
ignoreFailures
— 默认值:false-
如果此属性为
true
,则 Gradle 将在测试完成后继续进行项目的构建,即使其中一些测试失败了。请注意,默认情况下,Test
task 始终执行其检测到的每个测试,而与此设置无关。 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 起) -
'*' 通配符匹配
您可以在 build 脚本中或通过 --tests
命令行选项启用过滤。以下是在每次构建运行时应用的一些过滤器的示例
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")
}
}
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 脚本中声明过滤器的更多详细信息和示例,请参阅 TestFilter 参考。
命令行选项对于执行单个测试方法特别有用。当您使用 --tests
时,请注意 build 脚本中声明的包含项仍然有效。也可以提供多个 --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
task 生成以下结果
-
HTML 测试报告
-
XML 测试结果,格式与 Ant JUnit 报告 task 兼容 — 许多其他工具(例如 CI 服务器)都支持该格式
-
结果的高效二进制格式,
Test
task 使用该格式生成其他格式
在大多数情况下,您将使用标准 HTML 报告,该报告自动包含来自所有 Test
task 的结果,即使是您显式添加到构建中的那些 task。例如,如果您为集成测试添加了 Test
task,则如果运行了单元测试和集成测试 task,则报告将包含这两者的结果。
要聚合多个子项目的测试结果,请参阅 测试报告聚合插件。 |
与许多测试配置选项不同,有几个项目级 约定属性会影响测试报告。例如,您可以像这样更改测试结果和报告的目标位置
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())
}
}
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 task 类型,您可以使用它来生成自定义 HTML 测试报告。它只需要 destinationDir
的值以及您要包含在报告中的测试结果。这是一个示例,它为所有子项目的单元测试生成组合报告
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() })
}
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)
}
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)
}
}
// 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
task 的二进制测试结果。在聚合项目的 build 文件中,我们声明了 testReportData
配置,并依赖于我们要聚合结果的所有项目。Gradle 将自动从每个子项目中选择二进制测试结果变体,而不是项目的 jar 文件。最后,我们添加了一个 testReport
task,它聚合了来自 testResultsDirs
属性的测试结果,该属性包含从 testReportData
配置解析的所有二进制测试结果。
您应该注意,TestReport
类型组合了来自多个测试 task 的结果,并且需要聚合各个测试类的结果。这意味着,如果给定的测试类由多个测试 task 执行,则测试报告将包含该类的执行情况,但可能很难区分该类的各个执行情况及其输出。
通过 XML 文件将测试结果传达给 CI 服务器和其他工具
Test task 以 "JUnit XML" 伪标准创建描述测试结果的 XML 文件。JUnit 4、JUnit Jupiter 和 TestNG 测试框架都使用此标准,并且为所有这些框架使用相同的 DSL 块进行配置。CI 服务器和其他工具通常通过这些 XML 文件来观察测试结果。
默认情况下,文件写入到 layout.buildDirectory.dir("test-results/$testTaskName")
,每个测试类一个文件。可以为项目的所有测试 task 或每个测试 task 单独更改位置。
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
使用上述配置,XML 文件将写入到 layout.buildDirectory.dir("junit-xml/$testTaskName")
。
tasks.test {
reports {
junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
}
}
test {
reports {
junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
}
}
使用上述配置,test
task 的 XML 文件将写入到 layout.buildDirectory.dir("test-results/test-junit-xml")
。其他测试 task 的 XML 文件位置将保持不变。
配置选项
还可以配置 XML 文件的内容,通过配置 JUnitXmlReport 选项以不同的方式传达结果。
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
}
}
}
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 服务器理解此格式,它将指示测试是不稳定的。如果它不理解,它将指示测试成功,因为它将忽略 <flakyFailure>
信息。如果测试未成功(即,每次重试都失败),则无论您的工具是否理解此格式,都将指示测试失败。
禁用 mergeReruns
时(默认值),测试的每次执行都将列为单独的测试用例。
如果您使用 构建扫描 或 Develocity,则无论此设置如何,都会检测到不稳定的测试。
当使用 CI 工具时,启用此选项尤其有用,该 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 测试
-
最终继承自
TestCase
或GroovyTestCase
-
使用
@RunWith
注释 -
包含使用
@Test
注释的方法或超类
对于 TestNG,Gradle 会扫描使用 @Test
注释的方法。
请注意,抽象类不会执行。此外,请注意,Gradle 会向上扫描继承树到测试 classpath 上的 jar 文件中。因此,如果这些 JAR 包含测试类,它们也将被运行。
如果您不想使用测试类检测,可以通过将 Test 上的 scanForTestClasses
属性设置为 false
来禁用它。当您这样做时,测试 task 仅使用 includes
和 excludes
属性来查找测试类。
如果 scanForTestClasses
为 false 并且未指定任何 include 或 exclude 模式,则 Gradle 默认运行任何与模式 **/*Tests.class
和 **/*Test.class
匹配的类,排除与 **/Abstract*.class
匹配的类。
对于 JUnit Platform,仅使用 includes 和 excludes 来过滤测试类 — scanForTestClasses 无效。 |
测试日志记录
Gradle 允许对记录到控制台的事件进行微调控制。日志记录可以按每个日志级别进行配置,默认情况下,记录以下事件
当日志级别为 |
记录的事件 |
其他配置 |
|
无 |
无 |
|
异常格式为 SHORT |
|
|
堆栈跟踪被截断。 |
|
|
记录完整的堆栈跟踪。 |
可以通过调整测试 task 的 testLogging 属性中的相应 TestLogging 实例来修改每个日志级别的测试日志记录。例如,要调整 INFO
级别测试日志记录配置,请修改 TestLoggingContainer.getInfo() 属性。
测试分组
JUnit、JUnit Platform 和 TestNG 允许对测试方法进行复杂的分组。
本节适用于对服务于相同测试目的(单元测试、集成测试、验收测试等)的测试集合中的各个测试类或方法进行分组。有关根据测试类的目的划分测试类,请参阅孵化中的 JVM Test Suite 插件。 |
JUnit 4.8 引入了类别的概念,用于对 JUnit 4 测试类和方法进行分组。[1] Test.useJUnit(org.gradle.api.Action) 允许您指定要包含和排除的 JUnit 类别。例如,以下配置包含 CategoryA
中的测试,并排除 test
task 的 CategoryB
中的测试
tasks.test {
useJUnit {
includeCategories("org.gradle.junit.CategoryA")
excludeCategories("org.gradle.junit.CategoryB")
}
}
test {
useJUnit {
includeCategories 'org.gradle.junit.CategoryA'
excludeCategories 'org.gradle.junit.CategoryB'
}
}
JUnit Platform 引入了标签 (tagging) 来取代类别 (categories)。您可以通过 Test.useJUnitPlatform(org.gradle.api.Action) 指定包含/排除的标签,如下所示
TestNG 框架使用测试组 (test groups) 的概念来达到类似的效果。[2] 您可以通过 Test.useTestNG(org.gradle.api.Action) 设置配置在测试执行期间包含或排除哪些测试组,如下所示
tasks.named<Test>("test") {
useTestNG {
val options = this as TestNGOptions
options.excludeGroups("integrationTests")
options.includeGroups("unitTests")
}
}
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 支持
tasks.named<Test>("test") {
useJUnitPlatform()
}
tasks.named('test', Test) {
useJUnitPlatform()
}
有关更多详细信息,请参阅 Test.useJUnitPlatform()。
编译和执行 JUnit Jupiter 测试
要在 Gradle 中启用 JUnit Jupiter 支持,您只需添加以下依赖项
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
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 依赖项
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")
}
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-engine 和 junit-vintage-engine。 您还可以编写和插入您自己的 TestEngine
实现,如 此处 所述。
默认情况下,将使用测试运行时类路径上的所有测试引擎。 要显式控制特定的测试引擎实现,您可以将以下设置添加到您的构建脚本中
tasks.withType<Test>().configureEach {
useJUnitPlatform {
includeEngines("junit-vintage")
// excludeEngines("junit-jupiter")
}
}
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
将导致构建失败。
tasks.test {
useTestNG {
preserveOrder = true
}
}
test {
useTestNG {
preserveOrder = true
}
}
groupByInstance
属性控制测试是应按实例而不是按类分组。 TestNG 文档 更详细地解释了差异,但从本质上讲,如果您有一个依赖于 B()
的测试方法 A()
,则按实例分组可确保在下一个配对之前执行每个 A-B 配对,例如 B(1)
-A(1)
。 使用按类分组,将首先运行所有 B()
方法,然后再运行所有 A()
方法。
请注意,通常只有在您使用数据提供程序对其进行参数化时,才会有测试的多个实例。 此外,按实例分组测试是在 TestNG 6.1 版本中引入的。 为较旧的 TestNG 版本将 groupByInstances
属性设置为 true
将导致构建失败。
tasks.test {
useTestNG {
groupByInstances = true
}
}
test {
useTestNG {
groupByInstances = true
}
}
TestNG 参数化方法和报告
TestNG 支持 参数化测试方法,允许使用不同的输入多次执行特定的测试方法。 Gradle 在其测试方法执行报告中包含参数值。
给定一个名为 aTestMethod
的参数化测试方法,该方法接受两个参数,它将以名称 aTestMethod(toStringValueOfParam1, toStringValueOfParam2)
报告。 这使得很容易识别特定迭代的参数值。
配置集成测试
项目的常见要求是以某种形式合并集成测试。 他们的目标是验证项目的各个部分是否协同工作。 这通常意味着与单元测试相比,它们需要特殊的执行设置和依赖项。
将集成测试添加到构建的最简单方法是利用正在孵化的 JVM Test Suite 插件。 如果孵化解决方案不适合您,以下是您需要在构建中执行的步骤
-
为它们创建一个新的 源集
-
将您需要的依赖项添加到该源集的相应配置中
-
配置该源集的编译和运行时类路径
-
创建一个任务来运行集成测试
您可能还需要执行一些额外的配置,具体取决于集成测试的形式。 我们将在进行过程中讨论这些。
让我们从一个实际示例开始,该示例在构建脚本中实现前三个步骤,以新的源集 intTest
为中心
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")
}
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
的源集,该源集会自动创建
-
intTestImplementation
、intTestCompileOnly
、intTestRuntimeOnly
配置(以及 一些其他 不太常用的配置) -
一个
compileIntTestJava
任务,它将编译 src/intTest/java 下的所有源文件
如果您正在使用 IntelliJ IDE,您可能希望将这些附加源集中的目录标记为包含测试源而不是生产源,如 Idea 插件 文档中所述。 |
该示例还执行以下操作,并非所有这些操作都是您的特定集成测试所必需的
-
将
main
源集中的生产类添加到集成测试的编译和运行时类路径 —sourceSets.main.output
是包含已编译的生产类和资源的所有目录的 文件集合 -
使
intTestImplementation
配置从implementation
扩展,这意味着生产代码的所有声明的依赖项也成为集成测试的依赖项 -
对
intTestRuntimeOnly
配置执行相同的操作
在大多数情况下,您希望集成测试能够访问被测类,这就是为什么我们确保在示例中将这些类包含在编译和运行时类路径中的原因。 但是,某些类型的测试以不同的方式与生产代码交互。 例如,您可能有测试将您的应用程序作为可执行文件运行并验证输出。 对于 Web 应用程序,测试可以通过 HTTP 与您的应用程序交互。 由于在这些情况下,测试不需要直接访问被测类,因此您无需将生产类添加到测试类路径中。
另一个常见的步骤是将所有单元测试依赖项也附加到集成测试 — 通过 intTestImplementation.extendsFrom testImplementation
— 但这仅在集成测试需要所有或几乎所有单元测试具有的相同依赖项时才有意义。
您应该注意示例的其他几个方面
-
+=
允许您将路径和路径集合附加到compileClasspath
和runtimeClasspath
,而不是覆盖它们 -
如果您想使用基于约定的配置,例如
intTestImplementation
,您必须在新源集之后声明依赖项
创建和配置源集会自动设置编译阶段,但它在运行集成测试方面没有任何作用。 因此,难题的最后一部分是一个自定义测试任务,该任务使用来自新源集的信息来配置其运行时类路径和测试类
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) }
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 中工作正常。 |
黑盒集成测试
对于集成测试,您可以选择将测试集本身定义为附加模块。 您可以按照将主源转换为模块的方式执行此操作:通过将 module-info.java
文件添加到相应的源集(例如 integrationTests/java/module-info.java
)。
您可以在 此处 找到包含黑盒集成测试的完整示例。
在 Eclipse 中,目前不支持在一个项目中编译多个模块。 因此,此处描述的集成测试(黑盒)设置仅在 Eclipse 中有效,如果测试移动到单独的子项目中。 |
使用模块修补的白盒测试执行
白盒测试的另一种方法是通过将测试修补到被测模块中来保留在模块世界中。 这样,模块边界保持不变,但测试本身成为被测模块的一部分,然后可以访问模块的内部结构。
这在哪些用例中相关以及如何最好地完成这是一项讨论的主题。 目前还没有通用的最佳方法。 因此,Gradle 目前没有对此提供特殊支持。
但是,您可以像这样为测试设置模块修补
-
将
module-info.java
添加到您的测试源集,它是主module-info.java
的副本,其中包含测试所需的其他依赖项(例如requires org.junit.jupiter.api
)。 -
使用参数配置
testCompileJava
和test
任务,以使用测试类修补主类,如下所示。
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)
}
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 中看到无效的编译错误。 |
跳过测试
gradle build -x test
这排除了 test
任务以及它独占依赖的任何其他任务,即没有其他任务依赖于同一任务。 这些任务不会被 Gradle 标记为“SKIPPED”,而是根本不会出现在已执行任务的列表中。
通过构建脚本跳过测试可以通过几种方式完成。 一种常见的方法是通过 Task.onlyIf(String, org.gradle.api.specs.Spec) 方法使测试执行有条件。 如果项目具有名为 mySkipTests
的属性,则以下示例跳过 test
任务
tasks.test {
val skipTestsProvider = providers.gradleProperty("mySkipTests")
onlyIf("mySkipTests property is not set") {
!skipTestsProvider.isPresent()
}
}
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 地址或 "*"
(侦听所有接口)。
使用测试 fixtures
在单个项目中生成和使用测试 fixtures
测试 fixtures 通常用于设置被测代码,或提供旨在方便组件测试的实用程序。 Java 项目可以通过应用 java-test-fixtures
插件以及 java
或 java-library
插件来启用测试 fixtures 支持
plugins {
// A Java Library
`java-library`
// which produces test fixtures
`java-test-fixtures`
// and is published
`maven-publish`
}
plugins {
// A Java Library
id 'java-library'
// which produces test fixtures
id 'java-test-fixtures'
// and is published
id 'maven-publish'
}
这将自动创建一个 testFixtures
源集,您可以在其中编写测试 fixtures。 测试 fixtures 配置为
-
它们可以看到 main 源集类
-
test 源可以看到 test fixtures 类
例如,对于这个主类
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;
}
// ...
测试 fixture 可以在 src/testFixtures/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; }
// ...
声明测试 fixtures 的依赖项
与 Java Library Plugin 类似,测试 fixtures 公开 API 和实现配置
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")
}
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'
}
值得注意的是,如果依赖项是测试 fixtures 的实现依赖项,那么在编译依赖于这些测试 fixtures 的测试时,实现依赖项将不会泄漏到编译类路径中。 这可以更好地分离关注点并更好地避免编译。
使用另一个项目的测试 fixtures
测试 fixtures 不限于单个项目。 通常,依赖项目的测试也需要依赖项的测试 fixtures。 这可以使用 testFixtures
关键字非常容易地实现
dependencies {
implementation(project(":lib"))
testImplementation("junit:junit:4.13")
testImplementation(testFixtures(project(":lib")))
}
dependencies {
implementation(project(":lib"))
testImplementation 'junit:junit:4.13'
testImplementation(testFixtures(project(":lib")))
}
发布测试 fixtures
使用 java-test-fixtures
插件的优势之一是测试 fixtures 已发布。 按照约定,测试 fixtures 将与具有 test-fixtures
分类器的工件一起发布。 对于 Maven 和 Ivy,具有该分类器的工件只是与常规工件一起发布。 但是,如果您使用 maven-publish
或 ivy-publish
插件,则测试 fixtures 将作为 Gradle 模块元数据 中的附加变体发布,您可以直接依赖于另一个 Gradle 项目中的外部库的测试 fixtures
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"))
}
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 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
功能,这对于此库确实未定义。 这是因为按照约定,对于使用 java-test-fixtures
插件的项目,Gradle 会自动创建测试 fixtures 变体,其功能名称是主组件的名称,并带有附录 -test-fixtures
。
如果您发布库并使用测试 fixtures,但不希望发布 fixtures,则可以停用测试 fixtures 变体的发布,如下所示。 |
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
testng.xml
文件时测试排序的更多详细信息:http://testng.org/doc/documentation-main.html#testng-xml。