11、测试与报告:JUnit5集成、TestNG集成、测试过滤与并行执行、代码覆盖率报告(JaCoCo)
说实话,测试这块儿,很多团队把它当成「不得不做」的苦差事。但我个人的看法恰恰相反——没有一套靠谱的测试流水线,你的代码就像在走钢丝。Gradle 在测试方面的能力,其实被严重低估了。今天我就带你把它彻底盘清楚。
本章核心脉络:从测试框架集成,到执行策略优化,再到覆盖率可视化,一条线打通。
11.1 JUnit5 集成:从配置到实战
JUnit5 现在是 Java 生态的默认选择了。Gradle 从 4.6 版本开始原生支持,你几乎不需要额外插件。
最小配置长这样:
// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
test {
useJUnitPlatform()
}
嗯,就这么简单。但我在项目中遇到过一个问题:如果同时依赖了 JUnit4 和 JUnit5 的 jar,Gradle 会怎么处理? 答案是——它会优先使用 JUnit5 的引擎,但如果你混用了注解,可能会报错。我的建议是:新项目一律用 JUnit5,老项目迁移时用 @Tag 做渐进式替换。
小技巧:用 test { useJUnitPlatform { includeTags 'fast', 'unit'; excludeTags 'slow' } } 来按标签分组执行。我经常用这个在 CI 上先跑「fast」标签的用例,快速反馈。
11.2 TestNG 集成:另一种选择
TestNG 虽然热度不如从前,但在一些老项目里还是主力。Gradle 对 TestNG 的支持同样很成熟。
dependencies {
testImplementation 'org.testng:testng:7.8.0'
}
test {
useTestNG() {
suites 'src/test/resources/testng.xml'
parallel = 'methods'
threadCount = 4
}
}
这里有个坑——TestNG 的并行执行默认是「类级别」的。我曾经接手过一个项目,测试跑了 40 分钟,改成 parallel = 'methods' 后直接降到 12 分钟。你想想看,这差距有多大。
注意:JUnit5 和 TestNG 不要混在同一个模块里。Gradle 虽然能同时识别,但报告会分开生成,维护起来很头疼。我建议一个模块只选一个框架。
11.3 测试过滤:只跑你想要的
全量跑测试太慢了。Gradle 提供了非常灵活的过滤机制。
命令行过滤:
# 按类名模式匹配
./gradlew test --tests "com.example.service.*Test"
# 按完整类名
./gradlew test --tests "com.example.service.UserServiceTest"
# 按方法名
./gradlew test --tests "com.example.service.UserServiceTest.testLogin"
在 build.gradle 中配置:
test {
filter {
// 包含特定模式
includeTestsMatching "com.example.service.*"
// 排除特定模式
excludeTestsMatching "com.example.integration.*"
}
}
我个人习惯在本地开发时用 --tests "*UserService*" 这种模式,只跑我改过的模块。CI 上才全量跑。说白了,测试过滤不是为了偷懒,是为了缩短反馈循环。
11.4 并行执行:让多核 CPU 忙起来
单线程跑测试,你的 CPU 在打瞌睡。Gradle 支持两种并行方式:
| 方式 | 配置 | 说明 |
|---|---|---|
| 项目级并行 | org.gradle.parallel=true |
多模块项目同时构建 |
| 测试任务内并行 | test { maxParallelForks = 4 } |
单个模块内多线程跑测试 |
// gradle.properties
org.gradle.parallel=true
org.gradle.caching=true
// build.gradle
test {
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
forkEvery = 100 // 每100个测试用例重启一次JVM
}
这里有个经验值:maxParallelForks 不要超过 CPU 核心数的一半。我曾经设成和核心数一样,结果测试因为内存争抢反而变慢了。另外,如果你的测试有共享资源(比如数据库),并行执行前一定要确认隔离性,否则你会看到一堆诡异的失败。
避坑指南:我曾经在一个项目里开了 8 个 fork,结果测试报「端口已被占用」。排查了半天,发现是每个测试进程都试图启动同一个嵌入式数据库。解决方案:要么用随机端口,要么把这类测试放到单独的 integrationTest 任务里串行执行。
11.5 JaCoCo 代码覆盖率:不只是数字游戏
覆盖率不是越高越好,但没有覆盖率数据,你根本不知道哪些代码没被测试到。JaCoCo 是 Java 生态的事实标准。
集成配置:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.11"
}
test {
finalizedBy jacocoTestReport // 测试结束后自动生成报告
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = false
html.required = true
}
}
跑完 ./gradlew test jacocoTestReport 后,打开 build/reports/jacoco/test/html/index.html,你会看到这样的报告:
- 绿色:已覆盖的行
- 红色:未覆盖的行
- 黄色:部分覆盖(比如条件分支只测了一个分支)
我个人最关注的是「分支覆盖率」。行覆盖率 80% 可能很好看,但分支覆盖率只有 50% 的话,说明很多 if-else 逻辑根本没测到。说白了,分支覆盖率才是代码质量的照妖镜。
进阶用法:在 CI 中设置覆盖率阈值,低于阈值直接构建失败:
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.8 // 行覆盖率不低于 80%
}
limit {
counter = 'BRANCH'
minimum = 0.7 // 分支覆盖率不低于 70%
}
}
}
}
check.dependsOn jacocoTestCoverageVerification
11.6 整合:一个完整的测试配置模板
最后,给你一个我常用的完整配置。它把 JUnit5、并行执行、JaCoCo 都整合在一起了:
plugins {
id 'java'
id 'jacoco'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
test {
useJUnitPlatform()
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
forkEvery = 200
filter {
includeTestsMatching "com.example.*"
}
testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
showStandardStreams = true
}
finalizedBy jacocoTestReport
}
jacocoTestReport {
reports {
html.required = true
xml.required = true
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
counter = 'LINE'
minimum = 0.8
}
limit {
counter = 'BRANCH'
minimum = 0.7
}
}
}
}
check.dependsOn jacocoTestCoverageVerification
这个配置我用了两年多,在十几个项目里验证过。你直接复制过去,改一下包名就能用。嗯,测试这件事,自动化程度越高,你越能安心地睡个好觉。