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,让问题早点暴露。
小技巧: 如果你用 Mockito 或 MockK 来 mock Android 依赖,记得把 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 版本的签名、混淆、依赖都可能不同,测试当然也要分开。

配置测试变体,核心是 testBuildTypetestVariants

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 插桩测试配置 instrumentationTestRunner 代码覆盖率 Jacoco 配置与报告 测试变体 testBuildType / flavor 核心原则 测试配置要早做,覆盖率要合理,变体要隔离 别让测试成为「摆设」,要让它成为「安全网」

总结一下

这一章的内容,说白了就是三件事:

  1. 单元测试:用 testOptions 控制行为,跑得快,适合测逻辑。
  2. 插桩测试:用 testInstrumentationRunner 指定 runner,跑得慢,但测得真。
  3. 代码覆盖率:用 Jacoco 生成报告,帮你发现「哪些代码没被测到」。
  4. 测试变体:用 testBuildType 和源集隔离,让不同 flavor 的测试互不干扰。

嗯,配置这些其实不难,难的是坚持写测试、坚持看覆盖率报告、坚持在每次提交前跑一遍。我见过太多项目,测试配置写得漂漂亮亮,结果半年后覆盖率还是 0%。

别让你的测试成为摆设。从今天开始,每次改完代码,顺手跑一下测试。你会发现,bug 变少了,重构也敢做了。


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