25、Gradle测试与代码检查:testTask配置、单元测试与插桩测试、JaCoCo覆盖率、Detekt静态分析、Lint配置

说实话,很多团队把测试和代码检查当成「上线前的例行公事」。点一下按钮,跑一遍,绿了就过。但在我眼里,这一章才是真正决定代码质量的分水岭。你想想看,一个项目跑了两三年,如果没有自动化的测试和静态分析,那代码库迟早变成「屎山」。今天我就把Gradle里测试和代码检查的整套玩法,掰开了讲给你听。

25.1 testTask配置:掌控测试执行的生命周期

Gradle里的测试任务,核心就是Test这个类型。每个sourceSet都会自动生成一个对应的Test任务,比如test对应src/testtestDebug对应src/testDebug。我个人习惯,拿到一个新项目,第一件事就是打开build.gradle.kts,看看测试任务的配置。

// 配置单元测试任务
tasks.named<Test>("test") {
    useJUnitPlatform() // 使用JUnit5
    testLogging {
        events("passed", "skipped", "failed")
        showStandardStreams = true
        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
    }
    maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
    forkEvery = 100 // 每100个测试方法重启一次JVM
}

这里有个坑,我曾经遇到过:maxParallelForks设得太大,结果CI服务器上内存爆了。后来我学乖了,一般取CPU核心数的一半,再结合forkEvery控制JVM重启频率。说白了,测试并行度不是越大越好,得看你的测试有没有共享资源。

小技巧: 如果你用testFilter,可以只跑特定的测试类。比如filter { includeTestsMatching("*Login*") },调试时特别方便。

25.2 单元测试与插桩测试:两种测试,两种配置

Android项目里,测试分两大类:单元测试(Unit Test)和插桩测试(Instrumented Test)。单元测试跑在本地JVM上,速度快;插桩测试跑在真机或模拟器上,需要Android环境。

单元测试的配置,我刚才已经展示了。插桩测试呢?它对应的是connectedCheck任务链。你可以在defaultConfig里指定测试运行器:

android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        // 如果需要指定测试参数
        testInstrumentationRunnerArguments["clearPackageData"] = "true"
    }
}

嗯,这里要注意:插桩测试的依赖通常放在androidTestImplementation里,别放错了。我曾经见过一个同事把Espresso的依赖放到了testImplementation里,结果跑单元测试时疯狂报ClassNotFoundException。

测试类型 配置域 运行环境 典型任务
单元测试 testImplementation 本地JVM test, testDebug
插桩测试 androidTestImplementation 真机/模拟器 connectedCheck, connectedDebugAndroidTest

25.3 JaCoCo覆盖率:别让测试变成「假绿」

测试覆盖率这个东西,我一直觉得是「及格线」,不是「满分线」。80%的覆盖率不代表代码没bug,但20%的覆盖率一定说明你偷懒了。JaCoCo是Java/Kotlin生态里最主流的覆盖率工具,Gradle对它支持得很好。

启用JaCoCo很简单,先在build.gradle.kts里应用插件:

plugins {
    id("jacoco")
}

// 配置JaCoCo版本
jacoco {
    toolVersion = "0.8.11"
}

// 自定义覆盖率报告任务
tasks.register<JacocoReport>("jacocoTestReport") {
    dependsOn("testDebugUnitTest")
    reports {
        xml.required.set(true)
        html.required.set(true)
        csv.required.set(false)
    }
    // 指定要分析的源码和类文件
    sourceDirectories.setFrom(files("$projectDir/src/main/java"))
    classDirectories.setFrom(
        fileTree("$buildDir/tmp/kotlin-classes/debug") {
            exclude("**/R.class", "**/BuildConfig.class")
        }
    )
    executionData.setFrom(fileTree("$buildDir/jacoco") { include("*.exec") })
}
避坑指南: 我曾经因为Kotlin和Java的编译输出目录不同,导致JaCoCo报告一直显示0%。记住,Kotlin的class文件在tmp/kotlin-classes下,Java的在intermediates/javac下。两个目录都要配进去。

跑完./gradlew jacocoTestReport,打开build/reports/jacoco/html/index.html,就能看到详细的覆盖率报告。我个人习惯在CI流水线里加一个覆盖率阈值检查,低于80%直接构建失败:

tasks.register<JacocoCoverageVerification>("jacocoCoverageVerification") {
    violationRules {
        rule {
            limit {
                minimum = "0.8".toBigDecimal()
            }
        }
    }
}

25.4 Detekt静态分析:Kotlin代码的「照妖镜」

Detekt是专门为Kotlin设计的静态分析工具。它比Lint更懂Kotlin的语法特性,比如空安全、协程、密封类。我刚开始用Detekt时,被它报的几百个警告吓到了——原来我的Kotlin代码写得这么「Java风」。

配置Detekt也很简单:

plugins {
    id("io.gitlab.arturbosch.detekt").version("1.23.5")
}

detekt {
    buildUponDefaultConfig = true // 在默认规则基础上扩展
    allRules = false // 不启用所有规则(有些太严格)
    config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
    baseline = file("$rootDir/config/detekt/baseline.xml")
}

tasks.withType<Detekt>().configureEach {
    reports {
        html.required.set(true)
        xml.required.set(true)
        txt.required.set(false)
    }
}

你可以在detekt.yml里自定义规则。比如我团队里就禁用了MagicNumber规则,因为业务代码里很多常量是合理的。Detekt的规则分几个层级:

  • code-smell:代码坏味道,比如过长方法、过多参数
  • complexity:复杂度相关,比如圈复杂度、嵌套深度
  • style:代码风格,比如命名规范、空格缩进
  • performance:性能相关,比如不必要的对象创建
  • potential-bug:潜在bug,比如未处理的异常
我的经验: 别一开始就把所有规则都打开。先跑一次基线(detektBaseline),把现有问题标记为「已知」,然后逐步修复。否则团队会炸毛的。

25.5 Lint配置:Android原生的代码检查

Lint是Android SDK自带的静态分析工具,它主要检查Android相关的潜在问题:布局性能、资源冗余、权限声明、国际化等等。说实话,Lint和Detekt是互补的,一个管Android生态,一个管Kotlin语法。

配置Lint,通常在android块里:

android {
    lint {
        // 检查严重级别
        checkReleaseBuilds = false
        abortOnError = true
        warningsAsErrors = true
        // 忽略特定问题
        disable.add("MissingTranslation")
        disable.add("GoogleAppIndexingWarning")
        // 自定义检查配置
        lintConfig = file("$rootDir/config/lint/lint.xml")
    }
}

lint.xml里,你可以更精细地控制:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- 忽略所有模块的MissingTranslation -->
    <issue id="MissingTranslation" severity="ignore" />
    <!-- 将UnusedResources提升为error -->
    <issue id="UnusedResources" severity="error" />
</lint>

./gradlew lint,报告会生成在build/reports/lint-results.html。我个人习惯把Lint和Detekt都集成到pre-commit钩子里,提交代码前先过一遍,省得CI上反复失败。

25.6 知识体系总览

这一章的内容比较多,我画了一张图帮你理清关系:

Gradle测试与代码检查 testTask配置 useJUnitPlatform testLogging maxParallelForks 单元测试 vs 插桩测试 testImplementation androidTestImplementation 本地JVM 真机/模拟器 JaCoCo覆盖率 JacocoReport XML/HTML报告 覆盖率阈值 Detekt静态分析 Kotlin专属 自定义规则 基线管理 Lint配置 Android原生 lint.xml配置 warningsAsErrors

这张图把五个核心模块串起来了。从上到下,先配置测试任务,再区分单元测试和插桩测试,然后通过JaCoCo看覆盖率,最后用Detekt和Lint做静态分析。说白了,这就是一套完整的「代码质量流水线」。

我的建议: 别想着一步到位。先跑通单元测试和Lint,再逐步加入JaCoCo和Detekt。每加一个工具,给团队一周的适应期。我曾经在一个项目里同时上了四个检查工具,结果开发效率直接腰斩——后来我学乖了,循序渐进才是正道。

好了,这一章的内容就到这里。测试和代码检查,说白了就是「防患于未然」。你投入的时间,最终都会在线上bug的数量上体现出来。记住,好的代码不是写出来的,是「测」和「查」出来的。


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