CarSettings 单元测试落地方案
技术负责人视角 | v2 2026-06-28(review 修正版)
1. 背景与现状
1.1 项目背景
MiCarSettings 是小米汽车座舱核心设置应用,包含 16 个业务模块,承载了车辆控制、灯光、门窗锁控、充电、显示、驾驶等全部车控设置功能。由于长期高密度需求迭代,单元测试覆盖率几乎为零——仅 micarSnapshot 模块有占位测试。
1.2 已有工作
| CL | 状态 | 内容 |
|---|---|---|
| CL#320359 (+8883行) | NEW, patchset 7 | 测试框架基础设施 + gen-tests skill + 锁控/安全服务 case |
| CL#325528 (+2422行) | NEW, patchset 1 | 驾驶模块 18 个 Controller 测试 case |
1.3 已有框架评估
优点:
- Base test class 体系设计合理(BaseSettingsControllerTest → Switch/Tab/SeekBar 三层继承)
- VehiclePropertyProvider 参数化测试方案成熟(DFS 笛卡尔积生成信号组合)
- mock CarPropertyValue 完整 stub 四字段(propertyId, areaId, value, status)
- 反射注入 mock 依赖,避免 Robolectric 全量启动
- gen-tests skill 定义了清晰的模板和流程
需要改进的问题:
| 问题 | 影响 | 改进方案 |
|---|---|---|
| JUnit 4/5 混用 | gen-tests.md 模板用 JUnit 4,base class 用 JUnit 5 | 统一使用 JUnit 5 |
| Mock 框架混用 | gen-tests.md 用 mockito-kotlin,CL#325528 用 MockK | 统一使用 mockito-kotlin |
| 模板与 base class 脱节 | 模板不继承 base class,重复实现 mock 逻辑 | 模板改为继承 base class |
| 缺少 ButtonGroup 模板 | 20+ 子类无测试基类和模板 | 新增 BaseVehicleToggleGroupTest |
| AI-CR 反馈未处理 | mockkStatic 重复调用问题 | 在模板中消除此模式 |
2. 设计目标
2.1 核心目标
- 建立可复用的测试框架:base class + 模板 + 参数化,新增 Controller 测试 < 30 分钟
- 优先覆盖车控核心:一期聚焦三种基类类型的典型 Controller,验证框架可用
- 质量优先于数量:每个 Controller 覆盖信号驱动、用户操作、异常处理三条主线
- Daily 监控闭环:测试可定时运行,报告可追踪趋势,失败可飞书通知
2.2 分期目标
| 阶段 | 目标 | Controller 数 | Case 数 | 周期 |
|---|---|---|---|---|
| 一期 | 框架验证 + 典型 case + daily 跑通 | 20 | ~180 | 1-2 周 |
| 二期 | 三模块全量覆盖 + CI 门禁 | 95 | ~780 | 3-4 周 |
| 三期 | 全模块覆盖 + 覆盖率报告 | 150+ | ~1200 | 持续 |
3. 框架架构
3.1 分层设计
┌─────────────────────────────────────────────────────────────┐
│ 测试 Case 层 │
│ XxxControllerTest.kt (每个 Controller 一个测试文件) │
│ - 信号驱动测试 (参数化) │
│ - 用户操作测试 │
│ - 异常/边界测试 │
└────────────────────────┬────────────────────────────────────┘
│ 继承
┌────────────────────────▼────────────────────────────────────┐
│ 模板基类层 │
│ BaseVehicleSwitchTest<T> — 开关类 Controller │
│ BaseVehicleTabTest<T> — Tab 选项类 Controller │
│ BaseVehicleSeekBarTest<T> — 滑块类 Controller │
│ BaseVehicleToggleGroupTest<T>— 按钮组类 Controller [新增] │
└────────────────────────┬────────────────────────────────────┘
│ 继承
┌────────────────────────▼────────────────────────────────────┐
│ 基础设施层 │
│ BaseSettingsControllerTest<T> — 通用 Controller 测试基类 │
│ VehiclePropertyProvider — 参数化数据提供器 │
│ SettingsTestParams — 测试参数封装 │
│ VehiclePropertyValues — 参数化注解 │
│ TestReflectionHelper — 反射工具 │
└────────────────────────┬────────────────────────────────────┘
│ 依赖
┌────────────────────────▼────────────────────────────────────┐
│ 依赖层 │
│ JUnit 5 + Mockito 4 + Truth + Robolectric 4.11 │
│ test-dependencies.gradle (统一依赖配置) │
└─────────────────────────────────────────────────────────────┘
3.2 基类职责划分
BaseSettingsControllerTest(通用基类)
- Mock 注入:mPreference, mCarPropertyManager, context, fragmentController
- CarPropertyValue 完整 stub
- 反射工具方法
- 参数化测试执行框架
BaseVehicleSwitchTest(开关类)
- 继承 BaseSettingsControllerTest
- 状态追踪:isChecked, isEnabled
- 用户操作:userToggle(checked)
- 信号验证:verifyPropertyWritten(expectedValue)
BaseVehicleTabTest(Tab 选项类)
- 继承 BaseSettingsControllerTest
- 状态追踪:selectedTabIndex, allTabsDisabled
- 用户操作:selectTab(index)
- Tab 数据验证:tabValues 数组
BaseVehicleSeekBarTest(滑块类)
- 继承 BaseSettingsControllerTest
- 状态追踪:currentProgress, isSliderEnabled
- 用户操作:simulateUserDrag(progress)
- 值转换:propValToProgress / progressToPropVal
BaseVehicleToggleGroupTest(按钮组类)[新增]
- 继承 BaseSettingsControllerTest
- 状态追踪:selectedButtonIndex, groupEnabled
- 用户操作:selectButton(index)
- 多按钮状态验证
3.3 统一技术栈
| 组件 | 选型 | 版本 | 理由 |
|---|---|---|---|
| 测试框架 | JUnit 5 | 5.10.1 | 参数化支持好,扩展模型清晰 |
| Mock 框架 | mockito-kotlin | 5.2.1 | 与已有 base class 一致,语法简洁 |
| 断言库 | Google Truth | 1.1.5 | 可读性强,链式 API |
| Android 模拟 | Robolectric | 4.11.1 | 轻量级 Android 环境 |
| 覆盖率 | JaCoCo | 0.8.9 | Gradle 原生支持 |
统一不使用 MockK,避免 mockkStatic/unmockkStatic 的重复调用问题。
4. 测试策略
4.1 车控 Controller 通用测试矩阵
Switch 类测试矩阵
| Case ID | 场景 | 验证点 |
|---|---|---|
| SW-01 | 信号 ON → checked | isChecked = true |
| SW-02 | 信号 OFF → unchecked | isChecked = false |
| SW-03 | 信号不可用(STATUS_ERROR) → 置灰 | isEnabled = false |
| SW-04 | 非本 areaId → 忽略 | 无状态变化 |
| SW-05 | isSwitchEnabled=false → 禁用 | isEnabled = false |
| SW-06 | 用户打开 → 写入 ON | verify setCarProperty(PROP_ID, ON, AREA_ID) |
| SW-07 | 用户关闭 → 写入 OFF | verify setCarProperty(PROP_ID, OFF, AREA_ID) |
| SW-08 | 超时 → UI 回滚 | 恢复到操作前状态 |
| SW-09 | getPropertyIdSet 包含正确 PROP_ID | assertThat 包含 |
| SW-10 | 需要确认弹窗时 → 弹窗显示 | verify showDialog |
Tab 类测试矩阵
| Case ID | 场景 | 验证点 |
|---|---|---|
| TAB-01 | buildTabData 返回正确数量 | hasLength(N) |
| TAB-02 | Tab 数据值正确 | 每个 tab.data == TAB_VALUES[i] |
| TAB-03 | 信号匹配 Tab[i] → 选中 | selectedTabIndex = i |
| TAB-04 | 信号不匹配任何 Tab | 无选中 |
| TAB-05 | 信号不可用 → 全部置灰 | allTabsDisabled = true |
| TAB-06 | 用户选中 Tab[i] → 写入信号 | verify setCarProperty(PROP_ID, TAB_VALUES[i]) |
| TAB-07 | 点击 disabled Tab → 无操作 | 不触发 setCarProperty |
| TAB-08 | 超时 → 回滚到上次选中 | selectedTabIndex 恢复 |
| TAB-09 | Tab 依赖不可用 → 特定 Tab 置灰 | isTabDependencyAvailable = false |
SeekBar 类测试矩阵
| Case ID | 场景 | 验证点 |
|---|---|---|
| PB-01 | @PropertyRange 注解解析 | lower/upper/offset 正确 |
| PB-02 | 信号值在范围内 → slider 更新 | currentProgress = value + offset |
| PB-03 | 信号值超出范围 → 不更新 | currentProgress 不变 |
| PB-04 | 信号不可用 → slider 禁用 | isSliderEnabled = false |
| PB-05 | 用户拖动+松手 → 写入信号 | verify setCarProperty |
| PB-06 | 拖动中 → 不写入信号 | isTrackingTouch = true 时不触发 |
| PB-07 | 超时 → slider 回滚 | 恢复到操作前值 |
| PB-08 | propValToProgress 转换正确 | value + offset |
| PB-09 | progressToPropVal 转换正确 | progress - offset |
ButtonGroup 类测试矩阵
| Case ID | 场景 | 验证点 |
|---|---|---|
| TG-01 | 初始化 → 正确按钮数量 | buttons.size == N |
| TG-02 | 信号匹配 Button[i] → 选中 | selectedIndex = i |
| TG-03 | 信号不可用 → 全部禁用 | groupEnabled = false |
| TG-04 | 用户点击 Button[i] → 写入信号 | verify setCarProperty |
| TG-05 | 点击 disabled Button → 无操作 | 不触发 setCarProperty |
4.2 特殊逻辑扩展
| override 方法 | 追加 Case | 示例 |
|---|---|---|
| isSwitchEnabled() | 测试抑制原因映射 | inhibitReason != NO_INHIBIT → disabled |
| getSwitchDialogFragment() | 测试弹窗显示逻辑 | 开/关各一个弹窗 |
| onSwitchChanged() | 测试自定义写入逻辑 | 非标准 ON/OFF 值 |
| handleSwitchPropertyChange() | 测试多信号处理 | 多个 propId 分支 |
| isTabDependencyAvailable() | 测试 Tab 依赖条件 | 车速/档位等前置条件 |
| getAreaId() | 调整 AREA_ID 为实际值 | 门/窗/座椅区域 |
4.3 信号组合测试(DFS 全量遍历)
借鉴 PHUD 项目
CarPropertyProvider的 DFS 笛卡尔积方案,用于覆盖多信号依赖场景下的边界组合。
4.3.1 背景
MiCarSettings 中大量 Controller 存在多信号依赖关系:
isInnerLogicAvailable()依赖其他信号(如迎宾灯依赖氛围灯触发信号)isSwitchEnabled()依赖档位/车速等前置条件handleSwitchPropertyChange()处理多个 propId 分支
当前一期测试只验证单信号的 ON/OFF,未覆盖多信号交叉组合。PHUD 项目通过 DFS 算法自动生成所有信号值的笛卡尔积,确保每个组合至少被测试一次。
4.3.2 PHUD 方案核心
// PHUD 用法示例:DFS 自动生成 所有卡片类型 × 所有档位值 × POWER_MODE 的组合
@CarPropertyValues(
type = TestInstrumentCardType.ALL,
data = [
CarProp(PropertyIds.GEAR_STS, clazz = Driving.Gear::class), // 自动展开枚举所有值
CarProp(PropertyIds.POWER_MODE, intValues = intArrayOf(3)) // 固定值
]
)
fun gearIcon(name: String, params: PHudTestParams) = runTest(name, params) { ... }DFS 算法(来自 CarPropertyProvider.dfs()):
dfs(index):
if index == args.size:
emit(所有信号的当前组合)
return
for value in args[index].values:
cache.add(CarPropertyValue(propId, area, status, value))
dfs(index + 1)
cache.removeLast()
关键增强:@CarProp 的 clazz 参数支持自动展开枚举类所有静态字段,无需手动列 intValues。
4.3.3 MiCarSettings 适配方案
已实现(一期):以下基础设施已整合到一期 CL 中:
| 文件 | 路径 | 说明 |
|---|---|---|
VehiclePropertyValues.kt | com.micar.settings.test.annotation | JUnit 5 注解,声明信号 propId + 值列表 |
VehicleProp.kt | com.micar.settings.test.annotation | 注解数据类,包含 propId、areaId、status、intValues |
SettingsTestParams.kt | com.micar.settings.test.params | 参数化数据类,封装 Map<Int, SignalValue> |
VehiclePropertyProvider.kt | com.micar.settings.test.provider | JUnit 5 ArgumentsProvider,DFS 迭代式笛卡尔积算法 |
TestReflectionHelper.kt | com.micar.settings.test | 反射工具类,setField/getField/invokeMethod |
使用示例:
@ParameterizedTest(name = "{0}")
@VehiclePropertyValues(data = [
VehicleProp(propId = MAIN_PROP, intValues = [ON, OFF]),
VehicleProp(propId = DEPENDENCY_PROP, intValues = [0, 1, 2])
])
fun testMultiSignal(name: String, params: SettingsTestParams) = runTest(params) {
val mainValue = params.getValue(MAIN_PROP)
val depValue = params.getValue(DEPENDENCY_PROP)
// 验证 2×3 = 6 个组合的行为
}框架增强(二期):为 VehicleProp 注解新增 clazz 参数
// 当前 VehicleProp(仅支持手动列值)
@VehiclePropertyValues(data = [
VehicleProp(propId = PROP_ID, intValues = [CommonParams.OnOff.ON, CommonParams.OnOff.OFF])
])
// 增强后 VehicleProp(支持枚举自动展开)
@VehiclePropertyValues(data = [
VehicleProp(propId = PROP_ID, clazz = CommonParams.OnOff::class)
])VehiclePropertyProvider.provideArguments() 中增加 clazz 解析逻辑:
if (it.clazz != Any::class) {
// 反射提取枚举类所有 static int 字段
val values = it.clazz.java.declaredFields
.filter { f -> Modifier.isStatic(f.modifiers) }
.map { f -> f.get(null) as Int }
Arg(prop = it.propId, values = values)
} else {
Arg(prop = it.propId, values = it.intValues.toList())
}适用场景:
| 场景 | 示例 | 信号组合 |
|---|---|---|
| 开关 + 依赖信号 | 迎宾灯 = WELCOME_LIGHT_SWITCH × MOOD_LIGHT_TRIGGER | ON×可用, ON×不可用, OFF×可用, OFF×不可用 |
| Tab + 前置条件 | 门锁模式 = POWER_OPERATED_DOOR_MODE_STATUS × 车速信号 | 模式A×静止, 模式A×行驶, 模式B×静止, … |
| 多 propId 分支 | 后视镜折叠 = REAR_VIEW_MIRROR_FOLD × 状态机 | FOLD×已展开, UNFOLD×已折叠, … |
| 枚举全量覆盖 | 档位信号 = clazz = Driving.Gear::class | P/R/N/D/S/… 自动展开 |
4.3.4 实施节奏
| 阶段 | 内容 | 预计工作量 |
|---|---|---|
| 一期(当前) | 手动覆盖单信号 + 关键依赖组合(如迎宾灯×触发信号) | 已包含在 20 个 Controller case 中 |
| 二期框架优化 | VehicleProp.clazz 改造 + VehiclePropertyProvider 支持枚举展开 | 1-2 天 |
| 二期 case 扩展 | 对 isInnerLogicAvailable / handleSwitchPropertyChange 依赖场景使用 DFS 组合 | 随 Controller case 同步 |
| 三期全量 | 所有多信号 Controller 统一使用 DFS 组合模式 | 持续 |
4.3.5 一期已覆盖的多信号组合 case
虽然一期未启用 DFS 自动化,但以下 case 已手动覆盖了关键的多信号组合:
| Controller | 主信号 | 依赖信号 | 已覆盖组合 |
|---|---|---|---|
| WelcomeLightSwitchPrefController | WELCOME_LIGHT_SWITCH | MOOD_LIGHT_TRIGGER | ON×可用, ON×不可用, OFF×可用, OFF×不可用 |
| AtmosphereLightBrightnessProgressPrefController | MOOD_LIGHT_BRIGHTNESS | MOOD_LIGHT_TRIGGER | 范围内×可用, 范围内×不可用 |
| MirrorSettingController | REAR_VIEW_MIRROR_FOLD | — | FOLD/UNFOLD/UNKNOWN 状态机 |
| ElectricTailGatePreferenceController | ELECTRIC_TAIL_GATE | — | 7 种状态全覆盖 |
| DoorModeLockController | POWER_OPERATED_DOOR_MODE_STATUS | 车速/档位 | 模式切换×子项可见性 |
5. 一期:20 个典型 Controller
一期目标:验证框架可用、报告可出、daily 可跑。每种基类类型选 2-3 个最有代表性的 Controller。
5.1 车辆控制模块 — 6 个
| # | Controller | 类型 | 选择理由 |
|---|---|---|---|
| 1 | MirrorFoldWhenLockController | Switch | 后视镜折叠,典型开关,高频功能 |
| 2 | EasyEntrySwitchPrefController | Switch | 便利进出,典型开关 |
| 3 | MirrorAutoDownController | Tab | 倒车下翻,典型 Tab |
| 4 | WiperAdjustTabLayPrefController | Tab | 雨刮调节,典型 Tab |
| 5 | GloveBoxController | ToggleGroup | 手套箱,典型按钮组 |
| 6 | MirrorSettingController | ToggleGroup | 后视镜调节,典型按钮组 |
5.2 灯光模块 — 6 个
| # | Controller | 类型 | 选择理由 |
|---|---|---|---|
| 7 | HighBeamAutoAdjustPrefController | Switch | 远光自动,典型开关 |
| 8 | WelcomeLightSwitchPrefController | Switch | 迎宾灯开关,典型开关 |
| 9 | HeadLightsDelayTabLayoutPrefController | Tab | 大灯延时,典型 Tab |
| 10 | ExteriorLightsTabLayPreferenceController | Tab | 外部灯光,典型 Tab |
| 11 | AtmosphereLightBrightnessProgressPrefController | SeekBar | 氛围灯亮度,典型滑块 |
| 12 | KeyBgLightLevelPrefController | SeekBar | 按键背光,典型滑块 |
5.3 门窗锁控模块 — 8 个
| # | Controller | 类型 | 选择理由 |
|---|---|---|---|
| 13 | PGearAutoLockController | Switch | 已有 case,验证框架兼容 |
| 14 | WalkAwayLockController | Switch | 离车锁,高频功能 |
| 15 | CloseWindowWhenLockController | Switch | 锁车关窗,高频功能 |
| 16 | VehicleApproachUnLockController | Tab | 已有 case,验证框架兼容 |
| 17 | DoorModeLockController | Tab | 门锁模式,典型 Tab |
| 18 | TailGateHeightPrefController | SeekBar | 已有 case,验证框架兼容 |
| 19 | ChildLockCustomController | ToggleGroup | 儿童锁,典型按钮组 |
| 20 | ElectricTailGatePreferenceController | Switch | 电动尾门,高频功能 |
5.4 一期汇总
| 模块 | Controller 数 | Case 数 |
|---|---|---|
| VehicleBodyControl | 6 | ~55 |
| micarLightSettings | 6 | ~55 |
| micarLockSettings | 8 | ~70 |
| 合计 | 20 | ~180 |
5.5 一期验收标准
| 验收项 | 标准 | 验证方式 |
|---|---|---|
| 框架可用 | 20 个 Controller 测试全部通过 | ./gradlew testDebugUnitTest 0 failures |
| 报告可出 | 每次运行产出 HTML + JSON + XML | 检查 test-reports/ 目录 |
| daily 可跑 | cron 触发 + 结果归档 + 趋势记录 | 连续跑 3 天 |
| 覆盖率可查 | JaCoCo 报告包含目标模块 | 打开 jacoco/index.html |
| 失败可感知 | case 失败时飞书通知 | 模拟一个失败验证 |
6. 二期:三模块全量覆盖
二期目标:三个模块 95 个 Controller 全量覆盖,接入 CI 门禁,为后续推广到全模块积累经验。
6.1 二期范围
| 模块 | 一期已覆盖 | 二期新增 | 总 Controller | 总 Case |
|---|---|---|---|---|
| VehicleBodyControl | 6 | 24 | 30 | ~220 |
| micarLightSettings | 6 | 21 | 27 | ~240 |
| micarLockSettings | 8 | 30 | 38 | ~320 |
| 合计 | 20 | 75 | 95 | ~780 |
6.2 二期执行节奏
| 周次 | 工作内容 | 产出 |
|---|---|---|
| W1 | 门窗锁控剩余 30 个 Controller | ~250 case |
| W2 | 车辆控制剩余 24 个 Controller | ~165 case |
| W3 | 灯光剩余 21 个 Controller | ~185 case |
| W4 | 集成验证 + CI 门禁 + 覆盖率报告 | 全量通过 |
6.3 二期 CI 门禁
新增代码必须满足:
- 新增 Controller 必须附带测试 case
./gradlew testDebugUnitTest必须通过- 覆盖率不低于基线(一期建立的基准)
6.4 二期向主管汇报要点
- 框架验证结论:一期 20 个 Controller 验证了框架的可复用性和稳定性
- 覆盖率提升:从 0% 到三模块目标覆盖率
- Daily 监控效果:展示趋势图,说明稳定性改善
- 推广计划:三期扩展到全模块的路线图
- 投入产出比:框架化后新增 Controller 测试成本 < 30 分钟/个
7. 测试报告与 Daily CI
7.1 报告架构
┌─────────────────────────────────────────────────────────┐
│ Daily CI 报告架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ cron 触发 │───▶│ run_tests.sh │───▶│ 结果归档 │ │
│ │ 每天 9:00 │ │ 全模块测试 │ │ test-reports/ │ │
│ └──────────┘ └──────┬───────┘ │ daily/ │ │
│ │ │ YYYY-MM-DD/ │ │
│ ┌──────▼───────┐ └───────────────┘ │
│ │ 生成报告 │ │
│ │ - HTML (人看) │ │
│ │ - JSON (趋势) │ │
│ │ - XML (CI) │ │
│ │ - JaCoCo │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ 失败? → 飞书通知 │ │
│ │ webhook 推送 │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
7.2 报告目录结构
test-reports/
├── daily/
│ ├── 2026-06-29/
│ │ ├── test-result.xml # JUnit XML (CI 可读)
│ │ ├── test-report.html # HTML 报告 (人看)
│ │ ├── jacoco/ # 覆盖率报告
│ │ │ ├── index.html
│ │ │ └── report.xml
│ │ └── summary.json # 摘要 (趋势用)
│ ├── 2026-06-30/
│ └── ...
├── trend.json # 累积趋势数据
├── latest -> 2026-06-29 # 软链接到最新
├── run_daily_tests.sh # 每日运行脚本
├── generate_daily_summary.py # 摘要生成
├── update_trend.py # 趋势更新
└── notify_feishu.sh # 飞书通知脚本
7.3 summary.json 格式
{
"date": "2026-06-29",
"total": 180,
"pass": 178,
"fail": 2,
"skip": 0,
"duration_seconds": 45,
"coverage_lines_pct": 35.2,
"coverage_branches_pct": 28.1,
"failed_tests": [
{
"class": "MirrorFoldWhenLockControllerTest",
"method": "signalOn_switchChecked",
"error": "Expected true but was false"
}
]
}7.4 trend.json 格式
[
{"date": "2026-06-29", "total": 180, "pass": 178, "fail": 2, "pass_rate": 98.9},
{"date": "2026-06-30", "total": 180, "pass": 180, "fail": 0, "pass_rate": 100.0}
]7.5 飞书通知
触发条件:summary.json 中 fail > 0
通知内容:
⚠️ CarSettings 单元测试 Daily 报告 (2026-06-29)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计: 180 | 通过: 178 | 失败: 2 | 跳过: 0
通过率: 98.9% | 耗时: 45s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 失败 Case:
• MirrorFoldWhenLockControllerTest.signalOn_switchChecked
• WalkAwayLockControllerTest.userTurnsOff_propertySetToOff
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 详情: http://<机器IP>:8080/test-reports/daily/2026-06-29/
Webhook 配置:
# notify_feishu.sh
FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/<your-token>"
curl -X POST "$FEISHU_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"$MESSAGE\"}}"7.6 Daily 运行脚本核心逻辑
#!/bin/bash
# run_daily_tests.sh
set -e
DATE=$(date +%Y-%m-%d)
PROJECT_ROOT="/path/to/MiCarSettings"
REPORT_DIR="$PROJECT_ROOT/test-reports/daily/$DATE"
mkdir -p "$REPORT_DIR"
cd "$PROJECT_ROOT"
# 1. 运行测试(不因单个失败停止)
./gradlew testDebugUnitTest --continue 2>&1 | tee "$REPORT_DIR/build.log"
# 2. 收集 JUnit XML 结果
find . -path "*/test-results/*/*.xml" -exec cp {} "$REPORT_DIR/" \;
# 3. 生成 JaCoCo 报告
./gradlew jacocoTestReport 2>/dev/null || true
cp -r build/reports/jacoco "$REPORT_DIR/jacoco" 2>/dev/null || true
# 4. 生成 summary.json
python3 "$PROJECT_ROOT/test-reports/generate_daily_summary.py" "$REPORT_DIR"
# 5. 更新趋势
python3 "$PROJECT_ROOT/test-reports/update_trend.py" "$REPORT_DIR/summary.json"
# 6. 更新 latest 软链接
ln -sfn "$DATE" "$PROJECT_ROOT/test-reports/latest"
# 7. 失败通知
FAIL_COUNT=$(jq .fail "$REPORT_DIR/summary.json")
if [ "$FAIL_COUNT" -gt 0 ]; then
bash "$PROJECT_ROOT/test-reports/notify_feishu.sh" "$REPORT_DIR/summary.json"
fi
echo "Daily test completed: $DATE"7.7 Cron 配置
# 每天早上 9:07 运行(避开整点)
7 9 * * * cd /path/to/MiCarSettings && bash test-reports/run_daily_tests.sh >> /var/log/carsettings-test.log 2>&18. 测试生成工作流
8.1 gen-tests skill 改进
- 模板与 base class 统一:继承 BaseVehicleSwitchTest/TabTest/SeekBarTest
- JUnit 5 统一:去掉
@RunWith(RobolectricTestRunner::class) - Mockito 统一:去掉 MockK,统一 mockito-kotlin
- 新增 ToggleGroup 模板
- 扫描脚本集成:scan_module_controllers.py 驱动模板选择
8.2 单个 Controller 测试生成流程
读取源码 → 识别基类类型 → 提取关键信息 → 选择模板 → 填充变量 → 补充特殊逻辑 → 输出
8.3 批量生成策略
scan_module_controllers.py获取清单和分类- 按基类类型分组
- 读取源码提取 PROP_ID、AREA_ID、Tab values 等
- 按模板批量生成
- 追加 override 方法的额外 case
./gradlew :settingsPage:xxx:testDebugUnitTest验证
9. 风险与降级
| 风险 | 影响 | 降级策略 |
|---|---|---|
| Controller 依赖 CarService 太重 | 单测跑不了 | 用 mock 隔离,实在不行标记 @Disabled |
| Robolectric 对某些组件不支持 | 测试报错 | 换纯 JUnit + Mockito |
| 闲置电脑性能不足 | daily 跑太久 | 只跑 P0 Controller |
| 源码变更导致测试失效 | daily 红灯 | 先标记 @Disabled,不阻塞其他 case |
| 飞书 webhook 过期 | 通知收不到 | 脚本内置重试 + 日志记录 |
10. 关于车控测试 Skill
结论:改进现有 gen-tests.md,不新建。
改进点:模板继承 base class、统一 JUnit 5、新增 ToggleGroup 模板、集成扫描脚本。
触发场景:
| 用户说法 | 模式 |
|---|---|
| ”给 xxxController 写测试” | MODULE_PARTIAL |
| ”给 xxxSettings 模块补测试” | MODULE_ALL |
| ”针对 head commit 写测试” | HEAD_COMMIT |
11. 质量保障
11.1 测试质量标准
- 每个测试方法只验证一个行为
- 测试命名:
{场景}_{条件}_{预期结果} - mock 精确到方法级别
- 断言使用 Google Truth
- 不测试实现细节,只测试公开行为
11.2 评审标准
- 覆盖信号驱动、用户操作、异常处理三条主线
- 无冗余 case
- mock 不 over-mock
- 命名清晰可理解
12. 运行与验证
12.1 前置条件
# 1. 确保 CL#320359 已合入(提供 base class 和 test-dependencies.gradle)
git fetch origin && git log --oneline -5
# 2. 配置目标模块测试依赖(仅首次)
bash docs/test-infra/setup_all_modules_test.sh
# 3. 将测试 case 文件复制到对应模块
# test-cases/ 目录结构已与 MiCarSettings 模块结构对齐,直接复制即可
cp -r test-cases/VehicleBodyControl/src/test/java/* settingsPage/VehicleBodyControl/src/test/java/
cp -r test-cases/micarLightSettings/src/test/java/* settingsPage/micarLightSettings/src/test/java/
cp -r test-cases/micarLockSettings/src/test/java/* settingsPage/micarLockSettings/src/test/java/12.2 运行测试
# 运行全部测试
./gradlew testDebugUnitTest --continue
# 运行单个模块
./gradlew :settingsPage:VehicleBodyControl:testDebugUnitTest
./gradlew :settingsPage:micarLightSettings:testDebugUnitTest
./gradlew :settingsPage:micarLockSettings:testDebugUnitTest
# 运行单个测试类
./gradlew :settingsPage:micarLockSettings:testDebugUnitTest --tests "*.PGearAutoLockControllerTest"12.3 查看报告
# HTML 报告(浏览器打开)
open settingsPage/<module>/build/reports/tests/testDebugUnitTest/index.html
# JUnit XML(CI 解析用)
ls settingsPage/<module>/build/test-results/testDebugUnitTest/*.xml12.4 Daily CI 部署
# 1. 配置飞书 webhook
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
# 2. 手动验证一次
bash test-reports/run_daily_tests.sh
# 3. 配置 cron(每天 9:07)
crontab -e
# 添加: 7 9 * * * cd /home/dominic/home_ssd980/ai_lab && bash test-reports/run_daily_tests.sh >> /var/log/carsettings-test.log 2>&112.5 验收标准
| 验收项 | 标准 | 验证命令 |
|---|---|---|
| 编译通过 | 无编译错误 | ./gradlew testDebugUnitTest |
| Case 全部通过 | 0 failures | 检查 HTML 报告 |
| 报告可出 | XML + HTML + summary.json | ls test-reports/latest/ |
| 趋势可追 | trend.json 有数据 | cat test-reports/trend.json |
| 飞书可通知 | webhook 返回 200 | 手动触发一次 |
13. 执行计划总览
Phase 1:框架 + 典型 case + daily(本周)
- 新增 BaseVehicleToggleGroupTest 基类
- 统一 gen-tests.md 模板
- 更新 test-dependencies.gradle
- 为目标模块配置测试依赖(依赖 CL#320359 合入)
- 输出 20 个典型 Controller 的测试 case(170 cases)
- 搭建 daily CI 报告体系(脚本 + 飞书通知)
- 部署到闲置电脑,验证 daily 跑通
Phase 2:三模块全量覆盖(第 2-5 周)
- 门窗锁控剩余 30 个 Controller
- 车辆控制剩余 24 个 Controller
- 灯光剩余 21 个 Controller
- VehicleProp 新增
clazz参数,VehiclePropertyProvider 支持枚举自动展开(借鉴 PHUD CarPropertyProvider) - 多信号依赖 Controller 使用 DFS 笛卡尔积组合测试(§4.3)
- CI 门禁接入
- 覆盖率报告
Phase 3:全模块推广(持续)
- 扩展到驾驶、充电、显示等模块
- 覆盖率目标逐步提升