13、测试配置:JUnit5、TestNG、测试过滤与并行执行
测试配置这件事,说实话,很多项目前期都不太重视。我见过不少团队,测试框架能跑就行,管它是 JUnit 4 还是 5,反正绿了就完事。但等你项目大了,模块多了,测试用例上千条的时候——嗯,那时候再回头改配置,成本就高了。
今天咱们就聊聊 Gradle Kotlin DSL 里怎么配测试。我会把 JUnit5、TestNG、测试过滤、并行执行这几个点串起来讲。你想想看,这些东西其实都是围绕一个核心问题:怎么让测试跑得又快又准。
13.1 JUnit5 配置:从依赖到执行
JUnit5 现在是 Java 生态的标配了。Gradle 对它的支持也很成熟。我个人习惯用 testImplementation 和 testRuntimeOnly 来区分编译期和运行期的依赖。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
// 如果你需要参数化测试,再加这个
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
这里有个细节:useJUnitPlatform() 是必须写的。不写的话,Gradle 默认用 JUnit4 的引擎,你的 @Test 注解可能就识别不了。我在项目中遇到过有人把 JUnit5 的依赖加上了,但忘了配 useJUnitPlatform(),结果跑了一上午全是空的测试报告……
useJUnitPlatform() 配合 includeEngines 来指定引擎。但说实话,我建议统一升级到 JUnit5,混用容易出幺蛾子。
13.2 TestNG 配置:另一种选择
TestNG 虽然现在用得少了,但有些老项目或者特定场景(比如数据驱动测试)还是会选它。Gradle 对 TestNG 的支持也很直接。
dependencies {
testImplementation("org.testng:testng:7.8.0")
}
tasks.named<Test>("test") {
useTestNG {
// 指定测试套件 XML 文件
suites("src/test/resources/testng.xml")
// 并行执行配置
parallel = "methods"
threadCount = 4
}
}
TestNG 的并行执行能力其实比 JUnit5 原生要强一些。我记得以前做一个接口自动化项目,用 TestNG 的 parallel="methods" 配合 threadCount,把 2000 多个用例的执行时间从 40 分钟压到了 12 分钟。当然,前提是你的测试用例之间没有共享状态。
suites() 方法如果指定了 XML 文件,那么 parallel 和 threadCount 的配置会以 XML 文件中的为准。Gradle 脚本里的配置反而会被覆盖。我曾经因为这个排查了半天……
13.3 测试过滤:只跑你想要的
项目大了以后,全量跑测试太浪费时间。你改了一个模块的代码,只想跑跟这个模块相关的测试。Gradle 提供了很灵活的过滤机制。
tasks.named<Test>("test") {
// 按类名模式过滤
include("**/*ServiceTest*")
include("**/*RepositoryTest*")
exclude("**/*IntegrationTest*")
// 按注解过滤(需要 JUnit5 的 @Tag)
useJUnitPlatform {
includeTags("fast", "unit")
excludeTags("slow", "integration")
}
}
我个人比较推荐用 @Tag 注解的方式。为什么呢?因为类名模式匹配太死板了。你想想看,一个测试类里可能有几个快的用例和几个慢的用例,用类名过滤就做不到这么细。而 @Tag 可以打在方法级别上。
class UserServiceTest {
@Test
@Tag("fast")
fun `should create user`() {
// 快速用例
}
@Test
@Tag("slow")
fun `should sync user to remote`() {
// 慢速用例,依赖网络
}
}
然后在 Gradle 里,你可以为不同的场景定义不同的任务:
tasks.register<Test>("fastTest") {
group = "verification"
useJUnitPlatform {
includeTags("fast")
}
}
tasks.register<Test>("slowTest") {
group = "verification"
useJUnitPlatform {
includeTags("slow")
}
}
fastTest,CI 上跑全量。这样既保证了效率,又不牺牲质量。
13.4 并行执行:让多核 CPU 忙起来
现在的机器动不动就 8 核 16 核,但默认情况下 Gradle 是单线程跑测试的。这太浪费了。并行执行配置其实很简单,但坑也不少。
tasks.named<Test>("test") {
// 设置最大并行线程数
maxParallelForks = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
// 每个进程的 fork 次数(0 表示不限制)
forkEvery = 100
// JVM 参数隔离
jvmArgs("-Xmx512m")
}
maxParallelForks 控制的是进程级别的并行。每个进程跑一批测试用例。这里有个经验值:不要直接设成 CPU 核心数。为什么?因为你的机器还要跑 IDE、浏览器、数据库等等。我一般设为核心数减 1 或者核心数的一半。
forkEvery 这个参数很有意思。它表示每个 JVM 进程跑多少个测试用例之后重启。默认是 0(不限制)。但如果你有些测试用例会泄漏内存或者持有静态变量,建议设一个合理的值,比如 100。这样即使有内存泄漏,也不会影响后面的用例。
maxParallelForks 设成了 8,结果测试跑着跑着就 OOM 了。后来发现是每个进程默认的 JVM 堆大小是 512MB,8 个进程就是 4GB。解决方案是显式设置 jvmArgs("-Xmx256m"),或者降低并行数。
13.5 知识体系总览
下面这张图把测试配置的核心逻辑串起来了。你可以看到,从测试框架选择,到过滤策略,再到并行执行,每一步都有对应的 Gradle API。
13.6 综合示例:一个生产级的测试配置
最后,我把上面这些知识点揉在一起,给你一个完整的例子。这个配置我实际在几个中型项目里用过,效果还不错。
// build.gradle.kts
plugins {
java
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
val test by tasks.existing(Test::class)
// 快速测试任务
tasks.register<Test>("fastTest") {
group = "verification"
description = "只跑标记为 @Tag('fast') 的测试"
useJUnitPlatform {
includeTags("fast")
}
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
forkEvery = 200
jvmArgs("-Xmx256m")
}
// 全量测试任务(默认)
test.configure {
useJUnitPlatform {
excludeTags("slow")
}
maxParallelForks = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)
forkEvery = 100
jvmArgs("-Xmx512m")
// 测试报告
reports {
html.required.set(true)
junitXml.required.set(true)
}
// 测试监听
addTestListener(object : TestListener {
override fun beforeSuite(suite: TestDescriptor) {}
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
if (suite.parent == null) {
println("总用例数: ${result.testCount}")
println("通过: ${result.successfulTestCount}")
println("失败: ${result.failedTestCount}")
println("跳过: ${result.skippedTestCount}")
}
}
override fun beforeTest(testDescriptor: TestDescriptor) {}
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {}
})
}
这个配置里,fastTest 任务专门跑快速用例,适合本地开发时频繁执行。默认的 test 任务排除了慢速用例,但 CI 上可以单独加一个 slowTest 任务来覆盖全量。
嗯,关于测试配置,今天就聊这么多。记住一个原则:测试配置不是写一次就完事的。随着项目演进,你要不断调整并行数、过滤策略、JVM 参数。每次调整都记录一下原因和效果,这样团队其他人也能理解你的决策。