07 - 应用层测试框架
解决 P9(信号逻辑强依赖真车,难 CI/无车单测)。结论先行:现有
SettingsCarPropertyManager硬编码new VehicleControlManager(mContext)、无依赖注入,正经 JVM 单测必须先重构出 Repository 接口;短期可用 Robolectric+MockK 补 ViewModel 测,中期推 Hilt+FakeRepo,长期用 car-test-libFakeCar做 instrumentation。⭐ 本篇结论经 agent 重跑 + 项目代码核实,与 02 篇 的 car-test-lib 交叉印证。
1. 现状:封装层对测试不友好(代码核实)
| 问题 | 代码事实 | 影响 |
|---|---|---|
| 无依赖注入 | SettingsCarPropertyManager.kt:96 mCarPropMgr by lazy { VehicleControlManager(mContext) } 硬编码 | 外部无法注入 fake |
| 底层绑死 AOSP final 类 | VehicleControlManager extends NewBaseCarServiceManager<CarPropertyManager>,mCarServiceManager = car.getCarManager(PROPERTY_SERVICE) | 拿到的是 final 系统 CarPropertyManager,难 mock |
| Car 单例 | LocalCarManager.getInstance() 饿汉单例 | 测试无法替换 Car 实例 |
好消息:项目已有可复用的测试友好点:
SettingsCarPropertyManager.createCarPropertyValue(...)静态方法可构造CarPropertyValue;- 内置
mockErrorEvent/mockTimeoutEvent(见 01 篇 §3)——说明”绕过真实 CarService 测信号逻辑”在项目里已有雏形。
6 处硬编码全貌(均在 base/settingsVehicleLib/,是单测障碍的根):
CarPropertyMgrPreferenceController.java:513直接new SettingsCarPropertyManagerSettingsCarPropertyManager.kt:96mCarPropMgr by lazy { VehicleControlManager(mContext) }NewBaseCarServiceManager构造即LocalCarManager.getInstance().createCar(...)(饿汉单例,测试里永远不要实例化底层 manager)CarPropertyRepo(Kotlin object) init 块创建依赖(Flow 充电新模式)
现有测试范式(路已被验证可行):base/settingsVehicleLib/src/test/.../CarConfigManagerTest.kt 已用 mockStatic(GlobalCarPropertyManager) + mock(SettingsCarPropertyManager) 跑通 JVM 单测。全项目 19 个测试文件全是纯逻辑测试(密码/车身控制),零信号逻辑测试——下面路线与现有 Mockito 范式延续,团队认知为零。
2. 测试方案对比矩阵
| 方案 | 需真车 | 速度 | 改造量 | 覆盖深度 | 适配 MiCarSettings |
|---|---|---|---|---|---|
| Robolectric(自定义 shadow) | 否 | 极快(秒) | 中 | 业务逻辑/Fragment | 高(PreferenceFragment 友好) |
| MockK(mock 接口层) | 否 | 极快 | 大(需先抽象 Repository) | 纯业务逻辑 | 高 |
| Hilt + Fake Repository | 否(配 Robolectric) | 快 | 大(DI 改造) | 业务逻辑+Flow | 高(主推) |
| car-test-lib(FakeCar) | 否(emulator) | 中 | 小 | Manager 行为级 | 高(AAOS priv-app 金标准) |
| Espresso + Cuttlefish | 否(CF) | 慢 | 中 | UI+完整链路 | 中(CI 成本高) |
| JUnit5/Truth/Turbine | 否 | 极快 | 小(基础设施) | 取决于上层 | 高(必装) |
| JaCoCo/diff-coverage | 否 | — | 小 | 度量 | 高(CI 必装) |
3. 各方案详解
3.1 Robolectric(6021★)—— JVM 跑 Android
- 关键:没有官方
ShadowCarPropertyManager(android.car.*来自 car-lib,不在 frameworkandroid.jar,Issue #3937 已关闭——可加载类但语义 shadow 要自写)。 - 对 MiCarSettings 友好:能加载真实
Context/Resources/Preference/Fragment,PreferenceFragment 系设置应用很合适。 - 局限:AOSP 自己声明”Robolectric 测 CarService not officially supported”,CarService 测试已迁到 instrumentation。
- 仓库:https://github.com/robolectric/robolectric
3.2 MockK(5750★)/ Mockito(15444★)—— 对象级打桩
- 避坑:直接 mock
android.car.Car/CarPropertyManager这种 final 系统 class 大概率翻车(静态块触发ServiceManagernative 调用)。社区已踩坑。 - 正确用法:不要 mock 系统 final 类,而是抽
CarPropertyRepository接口包住它,mock 接口。MockK 对 Kotlin object/companion/coroutine 友好。 - 仓库:https://github.com/mockk/mockk
3.3 ⭐ Hilt + Fake Repository(官方推荐架构)
- 把信号源抽象成
CarPropertyRepository(Flow 输出),Hilt 注入;测试用@TestInstallIn换成 Fake:
// 生产
class CarPropertyRepositoryImpl @Inject constructor(...) : CarPropertyRepository {
fun gearState(): Flow<Int> = ...
}
// 测试
class FakeCarPropertyRepository : CarPropertyRepository {
private val gear = MutableStateFlow(0) // Turbine 推假信号
override fun gearState() = gear
}
@Reusable class TestModule @TestInstallIn(..., replaces = [CarPropertyModule::class]) { ... }- 改造量极大(不推全量):粗估 280+ 文件(
@HiltAndroidApp+ 所有 Fragment@AndroidEntryPoint+ Controller 链路@Inject+CarPropertyRepoobject→class),2 周以上,回归风险大。 - ⭐ 务实替代(推荐):不引入 Hilt,手写
IVehicleControl接口(把VehicleControlManager的 public 方法抽成接口),SettingsCarPropertyManager加@VisibleForTesting构造参数 + Kotlin 默认参数保持调用方兼容——<100 行改造,ROI 5/5。详见 §6 Phase 1。 - 文档:https://developer.android.com/training/dependency-injection/hilt-testing
3.4 ⭐ car-test-lib(FakeCar)—— AAOS priv-app 金钥匙
Google 官方 priv-app 测试 stub(packages/services/Car/libs/car-test-lib/):
val fakeCar = FakeCar.createCar(context)
val propMgr = fakeCar.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
// 背后是 FakeCarPropertyService,用 CarPropertyController 喂假信号
val controller = fakeCar.getCarManager(...) as CarPropertyController
controller.setProperty(0x11400400, 0, 60) // 灌车速=60- 官方维护、API 与 CarPropertyManager 同步演进,不用自己写 shadow。
- 注意:是 instrumentation test(需 emulator/Cuttlefish),非纯 JVM;要从 AOSP 编成 AAR 引入。
- 参考:
CarPropertyManagerTest.java(CarLibTests,官方 FakeCar 范例)。
3.5 Espresso + Cuttlefish(E2E)
- 车机 Instrumentation 必须跑在 AAOS emulator(Cuttlefish/DHU)或真车;标准 Android emulator 跑不起 CarPropertyManager。
- Spectatio(官方文档)是 Google AAOS E2E 框架。
- Espresso-Device 对折叠/旋屏支持,但车机多屏(InstrumentCluster/OccupantZone)覆盖不足。
3.6 基础设施:JUnit5 / Truth / Turbine / JaCoCo
- Turbine(2852★):测 Kotlin Flow 事实标准,
awaitItem()/awaitComplete()——把 CarProperty 回调包成 Flow 后用它断言信号序列,极适合测充电新模式(04 Flow 篇)。 - Truth(2787★):流式断言,失败信息友好。
- android-junit5(921★):需 AGP 8.0+/Gradle 8.0+(小米当前 Gradle 7.2/Kotlin 1.8.10,需评估升级)。
- JaCoCo/diff-coverage:CI 覆盖率;Kotlin 用 Kover(JetBrains 官方)更准。
4. 关键技术结论(避坑)
- Robolectric 无官方 ShadowCarPropertyManager——自写可行但不优雅。
- Mockito/MockK mock final 系统
CarPropertyManager大概率失败——必须抽接口。 - AOSP 官方不用 Robolectric 测 CarService——用
MockedVehicleHal+ instrumentation(MockedCarTestBase)。 - car-test-lib
FakeCar是 priv-app 无车测试官方金钥匙——优先用它而非自写 shadow。 - Cuttlefish 是 Android 16+ AOSP 唯一全功能虚拟设备——AAOS emulator 走它(见 04 篇)。
5. AOSP 参考案例(直接抄作业)
| 参考 | 路径 | 价值 |
|---|---|---|
| AOSP CarSettings | packages/apps/Car/Settings | 自带 Robolectric 单测,MiCarSettings 最直接参考 |
| AOSP Car Dialer | packages/apps/Car/Dialer/testing/ | 完整 Robolectric 套件(MockEntityFactory 等) |
| EmbeddedKitchenSinkApp | packages/services/Car/tests/EmbeddedKitchenSinkApp | AAOS priv-app 测试样本(com.google.android.car.kitchensink) |
| CarPropertyManagerTest | tests/CarLibTests/.../CarPropertyManagerTest.java | 官方 FakeCar+CarPropertyController 范例 |
6. MiCarSettings 分层落地建议(务实派路线)
| 阶段 | 周期 | 动作 | 改造量 | 产出 |
|---|---|---|---|---|
| Phase 0 零改造 | 1 周 | 加 Robolectric/Truth/Turbine/Kover 依赖;给 EnergyUtils(40+ 纯函数)、VehiclePropertyUtil 纯逻辑、RoundStrategy 补单测 | 极小 | 覆盖率 0→30%,建流程 |
| Phase 1 解锁信号逻辑 | 2 周 | ⭐手写 IVehicleControl 接口,SettingsCarPropertyManager 加 @VisibleForTesting 构造参数;测缓存/超时/分发;充电 Flow 链路补 Turbine 测 | <100 行 | 覆盖率 30→50%,信号核心可回归 |
| Phase 2 覆盖 VHAL 链路 | 1 月 | 抄 AOSP car-test-lib 14 个 Java 文件进项目(Apache 2.0),FakeCar+CarPropertyController 跑真实 CarPropertyManager 链路(支持小米 vendor prop) | 中(处理传递依赖) | 覆盖率 50→70%,无车端到端 |
| Phase 3 UI 验收 | 按需 | UI Automator 给核心交互路径(充电模式切换等)写 E2E,跑 Cuttlefish/真机 | 中 | 真机/CF 验收 |
Phase 1 核心改造(<100 行,ROI 最高)
// 新增接口
interface IVehicleControl {
val isConnected: Boolean
fun <T> getProperty(propId: Int, areaId: Int): CarPropertyValue<T>?
fun registerPropertyCallback(cb: CarPropertyEventCallback, propId: Int)
fun setIntProperty(propId: Int, propVal: Int, areaId: Int)
// ... 把 VehicleControlManager 的 public 方法搬过来
}
// VehicleControlManager 实现 IVehicleControl(不改业务)
// SettingsCarPropertyManager 改构造(默认参数保持调用方兼容)
class SettingsCarPropertyManager(
context: Context, lifecycle: Lifecycle? = null,
@VisibleForTesting internal val carPropMgr: IVehicleControl = VehicleControlManager(context)
)关键决策:不推 Hilt 全量
Hilt 全量改造 280+ 文件、2 周以上、回归风险大;手写 IVehicleControl 接口 <100 行解开 80% 障碍——务实派选后者。Hilt 仅在未来新代码(如 EnergyInfoViewModel)逐步引入构造注入时考虑。
AGP 7.2 硬约束(务必注意)
- JUnit5 instrumentation 需 AGP 8.2+ → 当前只能 JVM JUnit4 单测。
- Kover 不支持设备测覆盖率 → 仅 JVM;Instrumentation 覆盖率需另配 JaCoCo。
- 因此 Phase 0-2 全部锁定 JVM 单测,设备测留 Phase 3。
一句话:Phase 0 拿免费覆盖率(
EnergyUtils+Kover);Phase 1 用 <100 行IVehicleControl接口改造解锁 80% 单测(不上 Hilt);Phase 2 抄 FakeCar 覆盖 VHAL 链路。对现有代码侵入最小、ROI 最高,与团队现有 Mockito 范式一脉相承。