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 核心目标

  1. 建立可复用的测试框架:base class + 模板 + 参数化,新增 Controller 测试 < 30 分钟
  2. 优先覆盖车控核心:一期聚焦三种基类类型的典型 Controller,验证框架可用
  3. 质量优先于数量:每个 Controller 覆盖信号驱动、用户操作、异常处理三条主线
  4. Daily 监控闭环:测试可定时运行,报告可追踪趋势,失败可飞书通知

2.2 分期目标

阶段目标Controller 数Case 数周期
一期框架验证 + 典型 case + daily 跑通20~1801-2 周
二期三模块全量覆盖 + CI 门禁95~7803-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 55.10.1参数化支持好,扩展模型清晰
Mock 框架mockito-kotlin5.2.1与已有 base class 一致,语法简洁
断言库Google Truth1.1.5可读性强,链式 API
Android 模拟Robolectric4.11.1轻量级 Android 环境
覆盖率JaCoCo0.8.9Gradle 原生支持

统一不使用 MockK,避免 mockkStatic/unmockkStatic 的重复调用问题。

4. 测试策略

4.1 车控 Controller 通用测试矩阵

Switch 类测试矩阵

Case ID场景验证点
SW-01信号 ON → checkedisChecked = true
SW-02信号 OFF → uncheckedisChecked = false
SW-03信号不可用(STATUS_ERROR) → 置灰isEnabled = false
SW-04非本 areaId → 忽略无状态变化
SW-05isSwitchEnabled=false → 禁用isEnabled = false
SW-06用户打开 → 写入 ONverify setCarProperty(PROP_ID, ON, AREA_ID)
SW-07用户关闭 → 写入 OFFverify setCarProperty(PROP_ID, OFF, AREA_ID)
SW-08超时 → UI 回滚恢复到操作前状态
SW-09getPropertyIdSet 包含正确 PROP_IDassertThat 包含
SW-10需要确认弹窗时 → 弹窗显示verify showDialog

Tab 类测试矩阵

Case ID场景验证点
TAB-01buildTabData 返回正确数量hasLength(N)
TAB-02Tab 数据值正确每个 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-09Tab 依赖不可用 → 特定 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-08propValToProgress 转换正确value + offset
PB-09progressToPropVal 转换正确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()

关键增强@CarPropclazz 参数支持自动展开枚举类所有静态字段,无需手动列 intValues

4.3.3 MiCarSettings 适配方案

已实现(一期):以下基础设施已整合到一期 CL 中:

文件路径说明
VehiclePropertyValues.ktcom.micar.settings.test.annotationJUnit 5 注解,声明信号 propId + 值列表
VehicleProp.ktcom.micar.settings.test.annotation注解数据类,包含 propId、areaId、status、intValues
SettingsTestParams.ktcom.micar.settings.test.params参数化数据类,封装 Map<Int, SignalValue>
VehiclePropertyProvider.ktcom.micar.settings.test.providerJUnit 5 ArgumentsProvider,DFS 迭代式笛卡尔积算法
TestReflectionHelper.ktcom.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_TRIGGERON×可用, ON×不可用, OFF×可用, OFF×不可用
Tab + 前置条件门锁模式 = POWER_OPERATED_DOOR_MODE_STATUS × 车速信号模式A×静止, 模式A×行驶, 模式B×静止, …
多 propId 分支后视镜折叠 = REAR_VIEW_MIRROR_FOLD × 状态机FOLD×已展开, UNFOLD×已折叠, …
枚举全量覆盖档位信号 = clazz = Driving.Gear::classP/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主信号依赖信号已覆盖组合
WelcomeLightSwitchPrefControllerWELCOME_LIGHT_SWITCHMOOD_LIGHT_TRIGGERON×可用, ON×不可用, OFF×可用, OFF×不可用
AtmosphereLightBrightnessProgressPrefControllerMOOD_LIGHT_BRIGHTNESSMOOD_LIGHT_TRIGGER范围内×可用, 范围内×不可用
MirrorSettingControllerREAR_VIEW_MIRROR_FOLDFOLD/UNFOLD/UNKNOWN 状态机
ElectricTailGatePreferenceControllerELECTRIC_TAIL_GATE7 种状态全覆盖
DoorModeLockControllerPOWER_OPERATED_DOOR_MODE_STATUS车速/档位模式切换×子项可见性

5. 一期:20 个典型 Controller

一期目标:验证框架可用、报告可出、daily 可跑。每种基类类型选 2-3 个最有代表性的 Controller。

5.1 车辆控制模块 — 6 个

#Controller类型选择理由
1MirrorFoldWhenLockControllerSwitch后视镜折叠,典型开关,高频功能
2EasyEntrySwitchPrefControllerSwitch便利进出,典型开关
3MirrorAutoDownControllerTab倒车下翻,典型 Tab
4WiperAdjustTabLayPrefControllerTab雨刮调节,典型 Tab
5GloveBoxControllerToggleGroup手套箱,典型按钮组
6MirrorSettingControllerToggleGroup后视镜调节,典型按钮组

5.2 灯光模块 — 6 个

#Controller类型选择理由
7HighBeamAutoAdjustPrefControllerSwitch远光自动,典型开关
8WelcomeLightSwitchPrefControllerSwitch迎宾灯开关,典型开关
9HeadLightsDelayTabLayoutPrefControllerTab大灯延时,典型 Tab
10ExteriorLightsTabLayPreferenceControllerTab外部灯光,典型 Tab
11AtmosphereLightBrightnessProgressPrefControllerSeekBar氛围灯亮度,典型滑块
12KeyBgLightLevelPrefControllerSeekBar按键背光,典型滑块

5.3 门窗锁控模块 — 8 个

#Controller类型选择理由
13PGearAutoLockControllerSwitch已有 case,验证框架兼容
14WalkAwayLockControllerSwitch离车锁,高频功能
15CloseWindowWhenLockControllerSwitch锁车关窗,高频功能
16VehicleApproachUnLockControllerTab已有 case,验证框架兼容
17DoorModeLockControllerTab门锁模式,典型 Tab
18TailGateHeightPrefControllerSeekBar已有 case,验证框架兼容
19ChildLockCustomControllerToggleGroup儿童锁,典型按钮组
20ElectricTailGatePreferenceControllerSwitch电动尾门,高频功能

5.4 一期汇总

模块Controller 数Case 数
VehicleBodyControl6~55
micarLightSettings6~55
micarLockSettings8~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
VehicleBodyControl62430~220
micarLightSettings62127~240
micarLockSettings83038~320
合计207595~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 二期向主管汇报要点

  1. 框架验证结论:一期 20 个 Controller 验证了框架的可复用性和稳定性
  2. 覆盖率提升:从 0% 到三模块目标覆盖率
  3. Daily 监控效果:展示趋势图,说明稳定性改善
  4. 推广计划:三期扩展到全模块的路线图
  5. 投入产出比:框架化后新增 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.jsonfail > 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>&1

8. 测试生成工作流

8.1 gen-tests skill 改进

  1. 模板与 base class 统一:继承 BaseVehicleSwitchTest/TabTest/SeekBarTest
  2. JUnit 5 统一:去掉 @RunWith(RobolectricTestRunner::class)
  3. Mockito 统一:去掉 MockK,统一 mockito-kotlin
  4. 新增 ToggleGroup 模板
  5. 扫描脚本集成:scan_module_controllers.py 驱动模板选择

8.2 单个 Controller 测试生成流程

读取源码 → 识别基类类型 → 提取关键信息 → 选择模板 → 填充变量 → 补充特殊逻辑 → 输出

8.3 批量生成策略

  1. scan_module_controllers.py 获取清单和分类
  2. 按基类类型分组
  3. 读取源码提取 PROP_ID、AREA_ID、Tab values 等
  4. 按模板批量生成
  5. 追加 override 方法的额外 case
  6. ./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/*.xml

12.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>&1

12.5 验收标准

验收项标准验证命令
编译通过无编译错误./gradlew testDebugUnitTest
Case 全部通过0 failures检查 HTML 报告
报告可出XML + HTML + summary.jsonls 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:全模块推广(持续)

  • 扩展到驾驶、充电、显示等模块
  • 覆盖率目标逐步提升