17、测试构建:单元测试配置、插桩测试配置、代码覆盖率、测试变体
测试这件事,说实话,很多团队都是「嘴上说重要,实际能省则省」。但我做了这么多年构建系统,可以负责任地告诉你:没有自动化测试的构建流程,就像没有安全带的赛车——跑得再快,翻车也是迟早的事。
这一章,我们来聊聊 Android 构建系统中的测试配置。我会把单元测试、插桩测试、代码覆盖率以及测试变体这几个核心知识点串起来讲。嗯,都是我在项目中踩过坑之后总结出来的经验。
17.1 单元测试配置:testOptions
单元测试跑在本地 JVM 上,速度很快。你想想看,每次改完代码,点一下运行,几秒钟就能知道结果,这体验多好。
在 build.gradle 里,我们通过 testOptions 来配置单元测试的行为。我个人习惯把下面这段配置放在 android 块里:
android {
testOptions {
unitTests {
// 让单元测试能访问 Android 的资源文件
includeAndroidResources = true
// 是否让测试失败时继续运行后续测试
returnDefaultValues = false
}
}
}
几个关键点:
includeAndroidResources:设为true后,单元测试可以加载res/目录下的资源。我在项目中遇到过,如果不开启这个,测试里用到R.string.app_name就会直接崩掉。returnDefaultValues:默认是false。如果设为true,当调用 Android 框架方法时,会返回默认值(比如 0、null)而不是抛异常。这适合快速验证逻辑,但容易掩盖问题。我建议你保持false,让问题早点暴露。
includeAndroidResources 关掉,否则可能会引起类加载冲突。我曾经因为这个排查了整整一个下午。
17.2 插桩测试配置:instrumentationTestRunner
插桩测试跑在真机或模拟器上,能测试 UI 交互、数据库、传感器等真实环境。说白了,就是「真刀真枪」地测。
配置插桩测试,核心是指定 testInstrumentationRunner:
android {
defaultConfig {
// 使用 AndroidX 测试框架的默认 runner
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 也可以自定义 runner,比如集成 Espresso 或 UI Automator
// testInstrumentationRunner "com.example.MyTestRunner"
}
}
为什么需要自定义 runner? 举个例子,如果你的 App 在测试前需要初始化一些全局配置(比如注入 Mock 数据源),就可以写一个自定义 runner 继承 AndroidJUnitRunner,重写 onCreate() 方法。
testInstrumentationRunner 拼写错了——少写了一个 't'。嗯,这种低级错误,谁还没犯过呢?
17.3 代码覆盖率:Jacoco
代码覆盖率是个好东西,但它不是万能的。我见过有些团队把覆盖率指标定到 90% 以上,结果测试全是「只调用不断言」的假测试。说白了,覆盖率只能告诉你「哪些代码被执行了」,不能告诉你「执行得对不对」。
在 Android 构建系统中,我们通常用 Jacoco 来生成覆盖率报告。配置如下:
android {
buildTypes {
debug {
// 开启 Jacoco 覆盖率收集
testCoverageEnabled = true
}
}
}
然后,在 build.gradle 的顶层或模块级添加 Jacoco 插件:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.8"
}
// 自定义一个 task 来生成覆盖率报告
tasks.register('jacocoTestReport', JacocoReport) {
dependsOn 'testDebugUnitTest'
reports {
xml.required = true
html.required = true
}
// 指定源码和类文件路径
sourceDirectories.setFrom(files("$projectDir/src/main/java"))
classDirectories.setFrom(files("$projectDir/build/intermediates/javac/debug/classes"))
executionData.setFrom(files("$projectDir/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec"))
}
运行方式:
./gradlew jacocoTestReport
报告会生成在 build/reports/jacoco/jacocoTestReport/html/ 目录下,用浏览器打开就能看到详细的覆盖率数据。
testCoverageEnabled = true 后,构建速度会变慢,因为 Gradle 需要插入额外的字节码来记录执行路径。我建议只在 CI 上开启,本地开发时关掉。
17.4 测试变体:Test Variants
Android 构建系统支持多种变体(debug、release、flavor 等)。测试变体,说白了就是「为不同的构建变体配置不同的测试」。你想想看,debug 版本和 release 版本的签名、混淆、依赖都可能不同,测试当然也要分开。
配置测试变体,核心是 testBuildType 和 testVariants:
android {
// 指定测试使用的构建类型,默认是 "debug"
testBuildType = "debug"
// 也可以为每个 flavor 单独配置
flavorDimensions "version"
productFlavors {
demo {
dimension "version"
}
full {
dimension "version"
}
}
}
实际场景: 我在一个项目中,demo 版本和 full 版本的 API 地址不同。如果测试写死了 URL,切换 flavor 时就会跑错。解决方案是:为每个 flavor 创建对应的测试源集。
src/
├── main/
│ └── java/ # 公共代码
├── demo/
│ └── java/ # demo 专属代码
├── full/
│ └── java/ # full 专属代码
├── test/
│ └── java/ # 公共单元测试
├── testDemo/
│ └── java/ # demo 专属单元测试
└── testFull/
└── java/ # full 专属单元测试
这样,运行 ./gradlew testDemoDebugUnitTest 时,只会执行 testDemo 目录下的测试,不会和 full 版本的测试混淆。
知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一张「测试构建地图」:
总结一下
这一章的内容,说白了就是三件事:
- 单元测试:用
testOptions控制行为,跑得快,适合测逻辑。 - 插桩测试:用
testInstrumentationRunner指定 runner,跑得慢,但测得真。 - 代码覆盖率:用 Jacoco 生成报告,帮你发现「哪些代码没被测到」。
- 测试变体:用
testBuildType和源集隔离,让不同 flavor 的测试互不干扰。
嗯,配置这些其实不难,难的是坚持写测试、坚持看覆盖率报告、坚持在每次提交前跑一遍。我见过太多项目,测试配置写得漂漂亮亮,结果半年后覆盖率还是 0%。
别让你的测试成为摆设。从今天开始,每次改完代码,顺手跑一下测试。你会发现,bug 变少了,重构也敢做了。