智能座舱项目,车机大屏加仪表加副驾屏,版本迭代快得像坐火箭。每两周一个版本,发版前必做全量回归
那时候我组三个人,手工点一遍所有主流程,最少三天。点完手都麻了,结果还漏了几个冷门路径
我一度以为,自动化能救我们。可真做起来才发现,座舱的UI自动化比手机App难出一个量级
元素ID两周一变,CAN信号时序卡得死死的,多屏来回切。传统脚本写起来费劲,维护起来更费劲——往往改定位器的时间,比手工点还要久
直到我把 Cursor 和大模型塞进工作流,让 AI 来写、来修、来分析脚本。事情才真正拐了个弯
一、传统智能座舱UI自动化的瓶颈
为什么说智能座舱自动化难?
|
自动化要”点“按钮,得先”找到“按钮。在程序世界里,界面上每个元素——按钮、文本框、图标——在代码里都有一个专属的身份标识,叫 麻烦就麻烦在,车机系统 App 的 |
|
问答 问:那能不能不用“身份证号”,改用按钮上的文字找? 答:可以,但文字也可能改(比如”空调”改成”HVAC”),而且同屏可能有两个一样文字的按钮,照样抓不准。所以定位这件事,从来都是座舱自动化的头号难题。 |
① 元素定位脆弱:版本一更新脚本就报错
手机App用 Appium,resource-id 一般挺稳。但车机系统应用不一样
我们的车机是大厂定制的系统,每个版本重构一遍页面,连控件的 resource-id 都重新生成。比如空调按钮,上个版本叫 com.xxx.ac.btn_temp,下个版本变成 com.xxx.hvac.temp_btn
你写的二十条定位器,一次版本发布能挂一半。修定位器这件事,本身就成了测试的主要工作量
② CAN信号耦合:UI操作要同步验证信号层
座舱测试有个手机测试没有的硬需求:你点了屏幕上的空调按钮,得同时看 CAN 总线上有没有对应的报文出来
UI 层和信号层是耦合的。传统 Appium 只认屏幕,它根本感知不到 CAN 总线。你要在用例里手动接 python-can 去监听报文,代码量和耦合度直接翻倍
|
真实事故:有一次发版,空调开关在 UI 上看是亮的,但 CAN 报文迟迟没发。Appium 用例”绿”了,实际车上的压缩机根本没启动。后来靠人工在 CANoe 上抓包才发现问题。这说明——只测 UI 不测信号,座舱自动化就是半截子 |
③ 脚本维护成本高:修改用例比手工测试还慢
这是最要命的。一个迭代周期,新功能开发加旧功能回归,你要改用例、改定位器、改信号映射
我算过一笔账:我们组维护 300 条左右的核心用例,每个两周迭代平均有 60 条要动。一个人改定位器加调试,要花一天半。这还没算新用例编写
结果就是:自动化的维护成本,悄悄超过了手工测试本身
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
二、AI能做什么?Cursor + 大模型的3层能力
我是在一次技术分享上,听到有人用 Cursor 写前端代码。当时我就想:测试脚本能不能让 AI 来写?
试了三个月,我把 AI 在座舱自动化里的价值,归纳成三层。一层比一层深
这正是 AI 生成脚本的核心原理。你不用自己敲 Python,你只要说:”打开空调,调到 22 度,验证仪表同步显示。”大模型就能理解你的意思,吐出一段 uiautomator2 的操作代码。它不一定一次写对,但骨架和逻辑基本成型,你改改就能跑。大模型把”写脚本”这件事,从纯手写变成了”描述需求 + 校对“
|
问答 问:大模型会自己点车机屏幕吗? 答:不会,它只会写代码。真正去点屏幕的是 uiautomator2 这类框架,大模型是”写操作指南的人”,框架是”照着指南动手的人”。两者配合,才跑得起来 |
图片引自微信公众号,扫码关注阅读原文AI 三层能力架构(生成 → 修复 → 分析)
① AI生成脚本:描述场景直接出代码
第一层最简单。你用自然语言描述一个测试场景,比如”打开空调,设温度到22度,验证仪表同步显示”,AI 直接给你生成 Python + uiautomator2 的操作代码
我刚开始还不信,手写一条空调用例要半小时,AI 三十秒出初稿。当然初稿不能直接用,但改改就能跑,省掉的是最枯燥的样板代码
② AI修复脚本:元素定位失败自动重定位
第二层是救命的。脚本跑挂了,传统做法是你去看报错、去抓 DOM、去改 XPath。现在你把报错截图和 dump 出来的 XML 丢给 AI,它告诉你”按钮 resource-id 变了”,并给出新的定位器
③ AI分析失败:报告自动归类原因
第三层是进阶。一次回归跑完,几十条失败。AI 把失败日志吃进去,帮你分成三类:环境抖动、元素变更、真实 Bug
这一步省掉的是”人肉看日志”的大量时间。真实 Bug 被自动挑出来优先处理,环境类失败直接忽略重跑
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
小结:三层能力叠加,不是简单加法。生成省了写代码的时间,修复省了维护的时间,分析省了排查的时间。三者合起来,才把回归从三天压到四小时 |
三、环境搭建:工具链准备
光有想法不行,工具链得先立起来。我列一份我们组现在在用的清单。
先讲清楚这套工具链的”主力”——uiautomator2。它是 Google 提供的 Android UI 自动化框架,简单说,就是一套能让你用 Python 代码远程操控安卓界面的工具:点哪里、输什么字、往哪个方向滑,全都能用几行代码搞定
在车机这个场景,uiautomator2 更轻、更稳。uiautomator2 直接走 adb 通道,跟车机的契合度更高,装的环境也简单。
① Cursor 配置
Cursor 是主力编辑器。关键是那个 .cursorrules 文件——它告诉 AI 我们的车机测试规范、用什么 API、CAN 信号怎么约定
② Python 环境
|
|
|
|
pip install uiautomator2 python-can pytest allure-pytestpython -m uiautomator2 init
③ 车机连接
车机连电脑有两路:一路是 adb,控制屏幕;一路是 CAN,接 CANoe 或者 PCAN 硬件
# 1) adb 连车机(WiFi 调试,车机和电脑同一网段)adb connect 192.168.1.50:5555adb devices # 确认设备在线# 2) CAN 接口(以 PCAN-USB 为例,python-can 配置)# windows 下需安装 PCAN-Basic 驱动,channel 按硬件填
|
|
④ 目录结构规范
目录结构
|
ivitest/ # 项目根 ├── .cursorrules # AI 行为说明书(核心) ├── pages/ # POM 页面对象 │ ├── base_page.py │ ├── hvac_page.py # 空调页 │ └── cluster_page.py # 仪表页 ├── cases/ # pytest 用例 ├── can/ # python-can 封装 + DBC 文件 │ ├── dbc/xxx.dbc │ └── can_listener.py ├── data/ # Excel/JSON 测试数据 ├── reports/ # Allure 结果 └── conftest.py |
四、AI生成脚本实战:5步从需求到可运行用例
下面来真的。我们以”空调控制页测试”为例,走完整的五步
场景是:打开空调 → 设温度 22° → 验证仪表同步显示 22°这条用例要把 UI 操作和 CAN 信号验证绑在一起
图片引自微信公众号,扫码关注阅读原文正式走五步前,先讲一个关键概念:POM 模式(Page Object Model,页面对象模型)。它是自动化里常用的一种代码组织方式
啥意思?你把每个界面,封装成一个”类”。界面上的按钮,变成这个类的”属性”;对按钮的操作(点它、读它的值),变成类的”方法”。比如”空调页”就是一个类,里面有”开空调””设温度”这些方法。以后界面改了——比如按钮从减号改成下拉,你只在这个类里改一处,所有用到它的脚本都不用动
|
|
图片引自微信公众号,扫码关注阅读原文Step1:给 Cursor 描述测试场景
我在 Cursor 的聊天框里直接写:
|
# 我要测空调控制页: # 1. 打开空调主开关 # 2. 把温度设为 22 度 # 3. 验证仪表盘通过 CAN 报文同步显示目标温度 22 # 用 uiautomator2 操作 UI,用 python-can 监听信号,生成 POM 结构代码 |
Step2:AI 生成页面操作代码
AI 给了我空调页的 Page 类。核心部分(已加注释,可直接跑):
# pages/hvac_page.pyimport uiautomator2 as u2class HvacPage:"""空调控制页,封装所有 UI 操作"""def __init__(self, d: u2.Device):self.d = ddef open_ac(self):# 优先用 resource-id 定位,稳定self.d(resourceId="com.xxx.hvac:id/ac_main_switch").click()# 状态机等待:等开关真正变成开启态self.d(resourceId="com.xxx.hvac:Id/ac_main_switch").wait(timeout=3)def set_temp(self, temp: int):# 温度减到 22:先点减号若干次,再读当前值校验current = int(self.d(resourceId="com.xxx.hvac:id/temp_value").get_text())while current > temp:self.d(resourceId="com.xxx.hvac:id/temp_minus").click()current = int(self.d(resourceId="com.xxx.hvac:id/temp_value").get_text())if current <= temp:breakdef get_temp_display(self) -> int:return int(self.d(resourceId="com.xxx.hvac:id/temp_value").get_text())
Step3:AI 生成 CAN 信号验证代码
这一步是关键,把信号层接上。AI 按 DBC 文件生成了监听代码:
# can/can_listener.pyimport canclass CanListener:"""监听空调目标温度信号"""def __init__(self, channel="PCAN_USBBUS1", bitrate=500000):self.bus = can.interface.Bus(channel=channel,bustype="pcan",bitrate=bitrate)def wait_target_temp(self, expect: int, timeout=5) -> bool:# 按 DBC 解析:报文 ID 0x3A0,信号名 HVAC_TargetTempmsg = self.bus.recv(timeout=timeout)while msg is not None:if msg.arbitration_id == 0x3A0:# 信号起始位 8,长度 8bit,偏移 0,分辨率 0.5raw = (msg.data[1] >> 0) & 0xFFtemp = raw * 0.5if abs(temp - expect) < 0.6:return Truemsg = self.bus.recv(timeout=timeout)return Falsedef shutdown(self):self.bus.shutdown()
Step4:AI 生成 pytest 用例 + 断言
最后让 AI 把前面两块拼成一条完整用例(带断言)
# cases/test_hvac.pyimport pytestimport uiautomator2 as u2from pages.hvac_page import HvacPagefrom can.can_listener import CanListenerdef test_ac_set_temp_22():d = u2.connect("192.168.1.50:5555")hvac = HvacPage(d)can = CanListener()hvac.open_ac() # 开空调hvac.set_temp(22) # 设 22 度ui_temp = hvac.get_temp_display()assert ui_temp == 22, f"UI显示温度异常: {ui_temp}"# 关键:验证 CAN 信号层同步ok = can.wait_target_temp(22, timeout=5)assert ok, "仪表未通过CAN同步目标温度22°"can.shutdown()
Step5:运行验证 + AI 自动修复定位失败
第一次跑,大概率挂。我们的车机版本迭代把 temp_minus 改成了 temp_down。Cursor 捕获到 NoSuchElementError,我把截图和 hierarchy 丢回去,它自动把定位器改成新的,重跑通过
(540, 980)。车机分辨率一变,全错。后来我在 .cursorrules 里硬性禁用坐标,这事再没发生过 |
五、.cursorrules配置:让AI懂车机测试
这是全文我最想强调的一节:.cursorrules 就是写给 AI 的”团队规范”。你不写,AI 就按它自己的默认习惯来,生成的代码跟我们项目八竿子打不着
我贴一份我们迭代了十几个版本的配置,核心就五块
先解释下 .cursorrules 到底是个啥。Cursor 是一个 AI 编辑器,你在里面写代码时,AI 会实时帮你补全、生成。而 .cursorrules 是一个放在项目根目录的纯文本规则文件,专门写给这个 AI 看的”工作手册”
|
|
图片引自微信公众号,扫码关注阅读原文|
# .cursorrules —— 座舱UI自动化专用 # ① 技术栈声明 你正在编写车载座舱 UI 自动化测试代码。 技术栈固定为:uiautomator2(UI控制)+ python-can(信号监听) + pytest(用例)+ Allure(报告)。不要引入其他框架。 # ② 元素定位规范 – 优先使用 resource-id 定位,示例:d(resourceId=”com.xxx:Id/xxx”) – 次选 XPath 作为 fallback,禁止在 XPath 里写太长链式 – 严禁使用屏幕坐标 (x, y) 点击,分辨率会变 – 操作前用 wait(timeout=3) 做状态等待,不要 sleep 写死 # ③ CAN信号约定 – DBC 文件路径:can/dbc/vehicle.dbc – 信号命名遵循 DBC:如 HVAC_TargetTemp、AC_Status – CAN 接口默认 PCAN_USBBUS1,bitrate 500000 – 任何 UI 操作若对应信号,必须写 CAN 校验断言 # ④ 代码风格(POM分层) – 每个页面一个 Page 类,继承 BasePage – 页面类只封装操作,不写断言 – 断言只在 cases/ 下的 pytest 用例里写 – 函数/变量用 snake_case,类用 CamelCase # ⑤ 车机特殊处理 – 多屏切换:主驾屏/副驾屏/仪表,切换后必须重新 wait – 权限弹窗:首次进入可能弹”允许”对话框,需自动点掉 – 状态机等待:车机响应有延迟,用显式等待而非固定 sleep – 禁止生成会修改车机系统设置的破坏性代码 |
六、AI自愈机制:脚本挂了自己修
自愈,是我觉得 AI 给座舱自动化带来的最大变革。传统脚本一挂,人就得上。AI 自愈让脚本在断网前自己把命续上
讲自愈前,得先弄懂”元素定位失败”是啥。回到前面说的 resource-id:脚本上次记的是”空调按钮在第 3 排第 2 个”(或者说,身份证号是某个值)。它下次跑,就按这个老地址去找
结果这次界面改版了,按钮挪到了第 4 排。脚本还按老地址一摸——空的,找不到。这就是”元素定位失败”,传统脚本会直接报错退出,然后等你来救
|
生活类比:就像你常去一家店,记着”招牌在进门左转第二家”。某天店主把招牌挪到了右转第三家。你照老记忆走,扑了个空。传统脚本的做法是:空手回来跟你喊”没找到”。AI 自愈的做法是:站在店门口,扫一眼现在的布局,发现招牌挪了,自己走过去确认新位置,顺便把你的”记忆”更新成新地址,下次直接去对地方 |
整个机制跑起来是这样:
图片引自微信公众号,扫码关注阅读原文自愈异常处理代码
核心是一个装饰器/重试逻辑。定位失败后,自动把现场交给 AI:
# utils/self_heal.pyimport functools, tracebackfrom utils.ai_fix import ask_ai_for_locator # 封装对 Cursor/大模型的调用def self_heal(max_retry=2):def decor(func):def wrapper(*args, **kwargs):for i in range(max_retry):try:return func(*args, **kwargs)except Exception as e:# 1) 截图 + dump 当前页面 XMLd = args[0].dd.screenshot(f"heal_{func.__name__}_{i}.png")xml = d.dump_hierarchy()# 2) 把现场喂给 AI,拿新定位器new_loc = ask_ai_for_locator(str(e), xml)# 3) 用新定位器重写本次操作并重试if new_loc:args[0].apply_locator(new_loc)raise RuntimeError(f"自愈失败: {func.__name__}")return wrapperreturn decor
三个真实修复案例
案例1:版本升级后 resource-id 变更:
某次发布后,音乐卡片的 id 从 music_card 变成 media_card。AI 对比 XML,发现文本仍是”音乐”,推断控件被改名,给出新 locator,用例复活
案例2:弹窗遮挡:
进导航页先弹”同意隐私”对话框。AI 在截图里看到遮挡层,生成”先点允许再继续”的逻辑。比我们手动加弹窗处理稳多了
案例3:页面加载慢超时:
冷启动车机,首屏要 5 秒才出。原脚本 wait(3) 不够。AI 分析 XML 发现目标节点缺失,建议把等待改成”等元素出现为止”的显式等待,超时设 10 秒
七、CI/CD集成与数据驱动
脚本能自跑了,下一步是让它进流水线,每次提交自动跑。我们用的 Jenkins
先补个概念:CI/CD 是两套连在一起的做法。CI 是持续集成(Continuous Integration),意思是开发人员一提交代码,系统就自动把代码拉下来、自动构建、自动跑测试。CD 是持续交付(Continuous Delivery),在 CI 基础上,把通过测试的产物自动部署出去
放到座舱测试里,这套流程就是:你(或 Jenkins)一触发,系统自动连上车机,把全部用例跑一遍,自动出报告,再让 AI 把挂了的用例分分类、分析下原因。人不用守在旁边点,也不用自己挨个看日志。整条回归从”人手驱动”变成了”流水线驱动”
① Jenkins pipeline
# Jenkinsfile(节选)pipeline {agent anystages {stage('拉代码') { steps { git 'git@xxx:ivitest.git' } }stage('连车机') { steps { sh 'adb connect 192.168.1.50:5555' } }stage('跑pytest') {steps { sh 'pytest cases/ --alluredir=reports' }}stage('AI分析失败') {steps { sh 'python utils/ai_report.py reports' }}}post {always { sh 'allure generate reports -o allure-html' }}}
最后一步 AI 分析失败,就是把 Allure 结果喂给大模型做归类,生成一段”今日失败归因”发到群里
② 数据驱动:用 JSON 管理测试数据
空调的温度组合、风量档位、模式切换,排列起来几十种。手写用例太傻。我们用 JSON 管理,让 AI 按模板批量生成:
# data/hvac_cases.json{"cases": [{"temp": 22, "fan": 2, "mode": "auto"},{"temp": 26, "fan": 4, "mode": "face"},{"temp": 18, "fan": 1, "mode": "foot"}]}# cases/test_hvac_data.py —— pytest 参数化import json, pytestdata = json.load(open("data/hvac_cases.json"))def test_hvac_matrix(c):hvac.set_temp(c["temp"])hvac.set_fan(c["fan"])hvac.set_mode(c["mode"])assert can.wait_target_temp(c["temp"])
③ 多台架并行执行
一台车机跑全量要 4 小时。我们后来接了三台架,用 pytest-xdist 分片,1.5 小时搞定。AI 生成的用例因为结构统一,分片几乎零冲突
并行注意点:多台架共用 CAN 分析仪时要小心,信号会串。我们给每台架配独立 PCAN 通道,靠 .cursorrules 里的 channel 约定区分
八、效果对比、避坑与行动清单
这是我们组接 AI 自动化半年后的对比:
图片引自微信公众号,扫码关注阅读原文|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5个必须知道的坑
|
|
|
|
|
|
|
|
|
|
10条行动清单
-
装好 Cursor,建一个车机测试专用项目 -
写好第一版 .cursorrules,先把技术栈和禁用坐标写进去 -
用 uiautomator2 + python-can 跑通一条最小用例(含信号校验) -
把这条用例的结构(POM)固定下来,作为 AI 的”模板” -
加截图+XML 的自愈逻辑,让脚本能自己修定位 -
用 JSON/Excel 把测试数据抽出来,做参数化 -
接 Jenkins,每次提交自动跑回归 -
加一步 AI 失败归因,把报告发到团队群 -
多台架并行,把回归压到 2 小时内 -
每周复盘 AI 生成代码的失误,反哺 .cursorrules
