11、测试与报告:JUnit5集成、TestNG集成、测试过滤与并行执行、代码覆盖率报告(JaCoCo)

说实话,测试这块儿,很多团队把它当成「不得不做」的苦差事。但我个人的看法恰恰相反——没有一套靠谱的测试流水线,你的代码就像在走钢丝。Gradle 在测试方面的能力,其实被严重低估了。今天我就带你把它彻底盘清楚。

本章核心脉络:从测试框架集成,到执行策略优化,再到覆盖率可视化,一条线打通。

Gradle 测试与报告体系 Gradle Test 任务 测试框架集成 JUnit5 · TestNG 执行策略 测试过滤 · 并行执行 覆盖率报告 JaCoCo 集成 📊 HTML 测试报告 + 覆盖率报告 build/reports/ · build/reports/jacoco/

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

这个配置我用了两年多,在十几个项目里验证过。你直接复制过去,改一下包名就能用。嗯,测试这件事,自动化程度越高,你越能安心地睡个好觉


公众号:蓝海资料掘金营,微信deep3321