13、测试与代码检查:单元测试配置、Instrumentation测试、Lint配置、Detekt/Ktlint集成、测试报告聚合

说实话,测试和代码检查这件事,很多Android开发同学一开始都不太重视。我早年也是这样的——总觉得“功能跑通了就行”。直到有一次,一个同事改了一行工具类代码,结果导致十几个模块的单元测试全部挂掉,我们花了整整两天才定位到问题。嗯,从那以后,我再也不敢轻视测试和静态检查了。

这一章,我们就来聊聊如何在Gradle脚本里把测试和代码检查安排得明明白白。我会把单元测试、Instrumentation测试、Lint、Detekt、Ktlint,还有报告聚合这些事,一个一个说清楚。

核心要点:测试和代码检查不是“可有可无”的环节,它们是代码质量的守门员。配置得当,能帮你省下大量排查问题的时间。

测试与代码检查知识体系 Gradle 测试与检查 单元测试 (Unit Test) test 源码集 JUnit / Mockito testOptions 配置 Instrumentation 测试 androidTest 源码集 Espresso / UI Automator testInstrumentationRunner Lint 静态检查 lintOptions 配置 lint.xml 规则 abortOnError Detekt / Ktlint Kotlin 代码风格检查 detekt.yml 配置 ktlint 格式化 测试报告聚合 合并多个模块报告 HTML / XML 输出 CI 集成 自定义插件封装 统一配置入口 复用检查逻辑 团队规范落地

13.1 单元测试配置

单元测试是成本最低、收益最高的测试手段。说白了,就是测试你写的每一个函数、每一个类,在给定输入下是不是返回了预期结果。

在Android Gradle脚本里,单元测试默认就支持。你只需要在 build.gradle.kts 里加上依赖就行。我个人习惯把测试依赖统一放在一个 dependencies 块里,方便管理。

// app/build.gradle.kts
android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
    
    // 单元测试相关配置
    testOptions {
        unitTests.isReturnDefaultValues = true
        unitTests.isIncludeAndroidResources = true
    }
}

dependencies {
    // 单元测试
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.mockito:mockito-core:4.11.0")
    testImplementation("org.mockito:mockito-inline:4.11.0")
    testImplementation("org.robolectric:robolectric:4.9")
    
    // Instrumentation测试
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

小提示:unitTests.isReturnDefaultValues = true 这个配置,可以让未mock的方法返回默认值(比如0、null),而不是抛异常。我建议在开发阶段开启,但CI上最好关掉,避免掩盖问题。

你可能会问:为什么要加Robolectric?嗯,因为有些代码依赖Android框架的类(比如 ContextSharedPreferences),Robolectric可以在JVM上模拟这些环境,不用跑真机。

13.2 Instrumentation测试配置

Instrumentation测试,就是跑在真机或模拟器上的测试。它比单元测试更重,但能验证真实的UI交互和系统行为。

配置上,核心就是指定 testInstrumentationRunner。默认是 androidx.test.runner.AndroidJUnitRunner,如果你用了一些测试框架(比如Mockito的Android扩展),可能需要自定义Runner。

android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        // 如果需要传参数给测试
        testInstrumentationRunnerArguments["clearPackageData"] = "true"
    }
    
    // 测试构建类型
    testBuildType = "debug"
}

我在项目中遇到过一个问题:Instrumentation测试跑完后,app的数据还留在设备上,导致下一次测试结果不准。后来加了 clearPackageData = "true",每次测试前自动清除数据,问题就解决了。

注意:Instrumentation测试的源码放在 src/androidTest/ 目录下,和单元测试的 src/test/ 是分开的。别放错位置,否则Gradle找不到。

13.3 Lint配置

Lint是Android官方提供的静态代码分析工具。它能检查出潜在的性能问题、安全漏洞、甚至布局文件里的无用资源。

配置Lint,主要是在 lintOptions 块里做文章。我个人习惯把严重级别调高一点,宁可“误报”也不要漏报。

android {
    lintOptions {
        isAbortOnError = false
        isWarningsAsErrors = true
        isCheckReleaseBuilds = false
        xmlReport = true
        htmlReport = true
        xmlOutput = file("build/reports/lint/lint-results.xml")
        htmlOutput = file("build/reports/lint/lint-results.html")
        
        // 忽略某些检查
        disable("MissingTranslation")
        disable("GoogleAppIndexingWarning")
        
        // 将某些检查提升为错误
        error("ObsoleteSdkInt")
        error("UnusedResources")
    }
}

这里有个坑:isAbortOnError 如果设为 true,Lint发现任何错误都会让构建失败。我建议在本地开发时设为 false,在CI上设为 true。我曾经因为忘了改这个配置,导致团队里所有人一编译就报错,被吐槽了一整天。

13.4 Detekt / Ktlint 集成

Detekt和Ktlint都是Kotlin代码的静态分析工具。Detekt更侧重代码复杂度、代码规范;Ktlint则更像Kotlin版的ESLint,专注于代码风格。

集成方式很简单,用Gradle插件就行。我推荐用 io.gitlab.arturbosch.detektorg.jlleitschuh.gradle.ktlint 这两个插件。

// 根目录 build.gradle.kts
plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.0" apply false
    id("org.jlleitschuh.gradle.ktlint") version "11.3.1" apply false
}

// app/build.gradle.kts
plugins {
    id("io.gitlab.arturbosch.detekt")
    id("org.jlleitschuh.gradle.ktlint")
}

detekt {
    config = files("config/detekt/detekt.yml")
    buildUponDefaultConfig = true
    allRules = false
    reports {
        html.required.set(true)
        xml.required.set(true)
        txt.required.set(false)
    }
}

ktlint {
    verbose.set(true)
    android.set(true)
    outputToConsole.set(true)
    outputColorName.set("RED")
    reporters {
        reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.HTML)
        reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
    }
}

我的经验:Detekt的默认规则集比较严格,建议团队先讨论一下,把不合理的规则关掉。比如 MaxLineLength 这个规则,在Android项目里经常因为Lambda表达式太长而触发,我们团队就把它放宽到了140字符。

13.5 测试报告聚合

多模块项目里,每个模块都有自己的测试报告。一个个看太麻烦了。我们需要一个聚合报告,把所有模块的结果汇总到一起。

Gradle本身不提供聚合功能,但我们可以自己写一个任务。我常用的方案是:在根项目的 build.gradle.kts 里定义一个聚合任务,把各个模块的HTML报告复制到一个统一目录。

// 根目录 build.gradle.kts
tasks.register("aggregateTestReports") {
    group = "verification"
    description = "聚合所有模块的测试报告"
    
    val reportDir = layout.buildDirectory.dir("reports/tests")
    
    doLast {
        val aggregatedDir = reportDir.get().asFile
        aggregatedDir.mkdirs()
        
        subprojects.forEach { subproject ->
            val testReport = subproject.layout.buildDirectory
                .dir("reports/tests/testDebugUnitTest")
                .get().asFile
            
            if (testReport.exists()) {
                testReport.copyRecursively(
                    File(aggregatedDir, subproject.name),
                    overwrite = true
                )
            }
        }
        
        // 生成一个简单的索引页面
        val indexFile = File(aggregatedDir, "index.html")
        indexFile.writeText(buildIndexHtml(subprojects))
    }
}

fun buildIndexHtml(projects: Sequence<Project>): String {
    return """
    <html><body>
    <h1>测试报告聚合</h1>
    <ul>
    ${projects.joinToString("\n") { 
        "<li><a href='${it.name}/index.html'>${it.name}</a></li>" 
    }}
    </ul>
    </body></html>
    """.trimIndent()
}

同样的思路,也可以用来聚合Lint报告、Detekt报告。我个人习惯在CI上跑完所有检查后,自动生成一个聚合报告页面,团队成员打开就能看到所有模块的健康状况。

最佳实践:把测试和代码检查任务绑定到 check 任务上。这样执行 ./gradlew check 就能一键跑完所有检查。配置方式:

tasks.named("check") {
    dependsOn("detekt", "ktlintCheck", "lint", "test", "aggregateTestReports")
}

好了,这一章的内容就到这里。测试和代码检查配置好了,能帮你省下大量排查问题的时间。别嫌麻烦,这些配置都是一次性的,但收益是长期的。