1、课程介绍与环境搭建:了解 Binder Mock 回放的应用场景,搭建 Android 开发环境与 Compose 项目

各位同学,欢迎来到这门实战课。今天我们先不急着敲代码,而是把地基打牢。我会带你搞清楚 Binder Mock 回放到底能解决什么问题,然后咱们一起把开发环境搭起来。

一句话说透这门课的核心:用录制-回放的思路,把复杂的 Binder 跨进程调用 Mock 掉,让 Compose 的 UI 测试变得又快又稳。

1.1 为什么要学 Binder Mock 回放?

你想想看,平时写 Compose UI 测试时,最头疼的是什么?我个人觉得,十有八九是 Binder 调用。说白了,就是那些系统 Service、AIDL 接口,它们跑在另一个进程里。在单元测试的环境下,这些 Service 根本 unavailable。

我以前在做一个车机项目时,App 里大量调用了 WindowManager、InputManager 的服务。每次跑本地测试,看着那一堆 DeadObjectException,心态就崩。传统的 Mock 方案,比如 Mockito + PowerMock,能解决问题,但你要写无数个 when().thenReturn()。更坑的是,某个 Service 返回的对象结构复杂一点,你 Mock 出来的数据跟真实环境对不上,测试就白写了。

Binder Mock 回放这套思路,是我在一个跨团队的基础设施项目中实践出来的。它让你 先录制真实设备上的 Binder 调用流量,然后在测试环境里精确回放。

我的个人习惯:只要涉及系统 Service 的 Compose 测试,我优先用回放方案。因为它能保证测试数据跟线上环境一致,减少很多不必要的排障时间。

具体来说,这套方案有三大应用场景:

  • UI 自动化测试:用 Compose 写 UI 测试时,需要模拟系统服务的状态(比如权限弹窗、设备旋转、输入法切换)。
  • 集成测试:验证整个业务流程,但不想依赖真实的硬件或系统服务。
  • 回归测试:每轮发版前,跑一遍录制好的 Binder 回放,确保核心路径没有被改坏。

1.2 体系化认识:Binder Mock 回放的整体流程

在动手搭建环境前,我们先看一张总体的概念图。这张图是你学习后面所有章节的 导航仪。

Binder Mock 回放核心流程 生产端(App) Binder 代理 录制层 存储层 回放层 校验层 Compose Test 说明: 录制阶段在真机上运行 回放阶段在测试环境中 数据保持一致

这张图很关键。你跟着我来理解一下:

  1. 生产端:你的 App 代码发起 Binder 调用。
  2. Binder 代理:一个代理拦截器,它不修改原始调用,只是复制一份数据。
  3. 录制层:把入参、出参、异常信息全部抓下来。
  4. 存储层:保存成 JSON 或 Proto 文件,放到测试资源目录里。
  5. 回放层:在 Compose Test 中拦截相同的 Binder 调用,从录制文件中取出数据返回。
  6. 校验层:确保回放的数据结构跟录制时完全一致,防止版本升级导致兼容性问题。
  7. Compose Test:最终跑 UI 测试时,你的 App 以为自己真的跟系统服务通信,其实全是 Mock 数据。

1.3 Android 开发环境搭建

讲完理论,我们开始动手。环境这块,我建议你用最新的稳定版工具链。

环境要求速览表:

工具/组件 推荐版本 备注
Android Studio Ladybug 2024.2.1+ 必须支持 Compose 布局预览
Gradle 8.7+ 配置 KSP 插件使用
Kotlin 2.0.21 建议用 K2 编译器
Compose BOM 2024.12.01 统一管理 Compose 库版本
compileSdk / targetSdk 35 -
JUnit 5 5.11.0 用于测试用例

我的避坑指南:千万不要用 AGP 8.2 以下版本配合 Compose BOM 2024.x,会有 MissingClassException。我曾经因为这个踩过坑,折腾了一天多才发现是版本不匹配。

1.4 创建 Compose 项目与依赖配置

好,我们现在打开 Android Studio,新建一个 Empty Compose Activity 项目。包名我们用 com.example.binderreplay。然后我们来配置关键的依赖。

为什么我特意要提这一步?因为我发现很多同学在折腾 Binder 相关测试时,漏掉了一些必要的库,导致运行时报 NoClassDefFoundError。来看看 build.gradle.kts 的核心配置:

// 顶层 build.gradle.kts
plugins {
    id("com.android.application") version "8.7.0" apply false
    id("org.jetbrains.kotlin.android") version "2.0.21" apply false
    id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
    id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

// app/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    id("com.google.devtools.ksp")
}

android {
    namespace = "com.example.binderreplay"
    compileSdk = 35
    defaultConfig {
        minSdk = 26
        targetSdk = 35
    }
    buildFeatures.compose = true
}

dependencies {
    // Compose BOM
    val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
    implementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    // 测试核心
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
    testImplementation("io.mockk:mockk:1.13.12")

    // Binder Mock 回放核心库(后续章节我们会在本地发布)
    // testImplementation("com.example.binderreplay:replay-core:1.0.0")
}

特别注意:kotlinx-coroutines-test 的版本一定要跟主库的 coroutines 版本一致。我见过有人主库用 1.8.1,测试库用 1.9.0,结果 runTest 一直超时,排查了半天。

1.5 项目结构规划

说白了,一个好的项目结构能让你在写测试时少走很多弯路。我的习惯是,把测试相关代码严格分层:

com.example.binderreplay/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   └── java/.../        # 生产代码
│   │   ├── test/
│   │   │   └── java/.../        # 普通单元测试
│   │   └── replay/
│   │       ├── java/.../        # Binder 代理 & 回放逻辑
│   │       └── resources/       # 录制数据文件 (.json / .proto)
└── replay-core/                  # 独立模块,存放回放核心库

我建议你在创建项目时,先建一个独立的 replay-core Module。这样一来,回放核心库跟主 App 代码解耦,未来在其他项目里也能复用。第 3 章节我会带你具体实现这个模块。

1.6 章末测试

好,这一章的内容先到这里。我留几个问题,你可以在动手搭建环境后自测一下:

  1. Binder Mock 回放相比传统 Mockito Mock,最大的优势是什么?
  2. 录制层和回放层之间,通过什么机制保证调用顺序的一致性?
  3. 如果 Compose BOM 版本跟 Kotlin 版本不匹配,会出现什么典型错误?