07 - 应用层测试框架

解决 P9(信号逻辑强依赖真车,难 CI/无车单测)。结论先行:现有 SettingsCarPropertyManager 硬编码 new VehicleControlManager(mContext)、无依赖注入,正经 JVM 单测必须先重构出 Repository 接口;短期可用 Robolectric+MockK 补 ViewModel 测,中期推 Hilt+FakeRepo,长期用 car-test-lib FakeCar 做 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 SettingsCarPropertyManager
  • SettingsCarPropertyManager.kt:96 mCarPropMgr 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

  • 关键没有官方 ShadowCarPropertyManagerandroid.car.* 来自 car-lib,不在 framework android.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 大概率翻车(静态块触发 ServiceManager native 调用)。社区已踩坑。
  • 正确用法:不要 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 + CarPropertyRepo object→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. 关键技术结论(避坑)

  1. Robolectric 无官方 ShadowCarPropertyManager——自写可行但不优雅。
  2. Mockito/MockK mock final 系统 CarPropertyManager 大概率失败——必须抽接口。
  3. AOSP 官方不用 Robolectric 测 CarService——用 MockedVehicleHal + instrumentation(MockedCarTestBase)。
  4. car-test-lib FakeCar 是 priv-app 无车测试官方金钥匙——优先用它而非自写 shadow。
  5. Cuttlefish 是 Android 16+ AOSP 唯一全功能虚拟设备——AAOS emulator 走它(见 04 篇)。

5. AOSP 参考案例(直接抄作业)

参考路径价值
AOSP CarSettingspackages/apps/Car/Settings自带 Robolectric 单测,MiCarSettings 最直接参考
AOSP Car Dialerpackages/apps/Car/Dialer/testing/完整 Robolectric 套件(MockEntityFactory 等)
EmbeddedKitchenSinkApppackages/services/Car/tests/EmbeddedKitchenSinkAppAAOS priv-app 测试样本(com.google.android.car.kitchensink
CarPropertyManagerTesttests/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 范式一脉相承。