13、测试与代码检查:单元测试配置、Instrumentation测试、Lint配置、Detekt/Ktlint集成、测试报告聚合
说实话,测试和代码检查这件事,很多Android开发同学一开始都不太重视。我早年也是这样的——总觉得“功能跑通了就行”。直到有一次,一个同事改了一行工具类代码,结果导致十几个模块的单元测试全部挂掉,我们花了整整两天才定位到问题。嗯,从那以后,我再也不敢轻视测试和静态检查了。
这一章,我们就来聊聊如何在Gradle脚本里把测试和代码检查安排得明明白白。我会把单元测试、Instrumentation测试、Lint、Detekt、Ktlint,还有报告聚合这些事,一个一个说清楚。
核心要点:测试和代码检查不是“可有可无”的环节,它们是代码质量的守门员。配置得当,能帮你省下大量排查问题的时间。
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框架的类(比如 Context、SharedPreferences),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.detekt 和 org.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")
}
好了,这一章的内容就到这里。测试和代码检查配置好了,能帮你省下大量排查问题的时间。别嫌麻烦,这些配置都是一次性的,但收益是长期的。