24、测试与仿真:车载模拟器、VHAL 模拟、自动化测试框架
说实话,做 Automotive 开发最头疼的不是写代码,而是「没车可跑」。
你想想看,每次改一行 VHAL 的代码,都要真车验证?那项目周期得拖到猴年马月去。所以这一章,我就跟你聊聊怎么在没车的情况下,把活儿干得漂漂亮亮。
24.1 车载模拟器:你的虚拟测试场
Android Automotive 的模拟器,说白了就是一个跑在 PC 上的「虚拟座舱」。它跟手机模拟器最大的区别是:它得模拟车特有的硬件,比如空调、车窗、仪表盘。
我个人习惯用 Android Studio 自带的 Automotive 模拟器。创建时选一个 Automotive 镜像,比如 API 34 的。启动后你会看到一个横屏的车机界面,左边还有一排虚拟按键——那是模拟的「硬按键」,比如音量、Home、Back。
核心要点:模拟器里跑的其实是完整的 Android Automotive 系统,包括 SystemUI、Launcher、CarService。你可以在上面装 APK、调布局、测多屏交互。
但模拟器有个局限:它默认的 VHAL 是「假」的。你点一下空调按钮,它不会真的制冷。所以我们需要更深入的模拟——VHAL 模拟。
24.2 VHAL 模拟:让虚拟车「动」起来
VHAL(Vehicle Hardware Abstraction Layer)是车机和硬件之间的桥梁。在模拟器里,VHAL 默认跑的是一个 MockVhal 实现。它只返回一些静态值,比如车速永远是 0,车门永远是关的。
这显然不够用。我曾在项目里需要模拟「车速从 0 加速到 120 km/h」的场景,用来测试仪表盘的动画流畅度。MockVhal 做不到,所以我得自己写一个模拟器端的 VHAL 插件。
24.2.1 自定义 VHAL 属性注入
你可以通过 adb 命令直接往 VHAL 里写属性值。比如:
# 模拟车速 60 km/h
adb shell dumpsys car_service --set-property vehicle_speed:0:60.0
# 模拟左转向灯开启
adb shell dumpsys car_service --set-property turn_signal:0:1
但这种方式只能改单个属性,而且没法做时序变化。要模拟「加速过程」,我建议用脚本循环写值:
#!/bin/bash
# 模拟从 0 加速到 120,每 0.1 秒增加 5 km/h
for speed in $(seq 0 5 120); do
adb shell dumpsys car_service --set-property vehicle_speed:0:$speed
sleep 0.1
done
我的经验:这种脚本在调试仪表盘时特别有用。我曾经用它发现了一个仪表盘指针回弹的 bug——指针在减速时会有 0.5 秒的延迟,后来发现是 VHAL 回调线程阻塞了。
24.2.2 使用 VHAL 模拟器插件
如果你需要更复杂的模拟(比如 GPS 轨迹、空调温度联动),可以写一个 VHAL 模拟器插件。它本质上是一个 Java 类,实现了 IVehicle 接口,然后通过 VehicleHalServer 注册到 CarService 里。
我简单列一下步骤:
- 创建一个 Android Library 模块,依赖
android.car和android.car.hal。 - 实现
VehicleHal抽象类,重写getAllProperties、set、get等方法。 - 在
onCreate里启动一个定时器,定期更新属性值(比如每 100ms 更新一次车速)。 - 编译成 APK,push 到模拟器的
/system/priv-app目录下,重启 CarService。
注意:模拟器插件的签名必须和系统签名一致,否则 CarService 会拒绝加载。我踩过这个坑,折腾了半天才发现是签名问题。
24.3 自动化测试框架:让机器替你跑测试
手动测试一次两次还行,但回归测试时,你总不能每次都点一遍空调、车窗、导航吧?这时候就需要自动化测试框架上场了。
Android Automotive 的自动化测试,主要分三层:
| 层级 | 工具/框架 | 测试对象 |
|---|---|---|
| UI 层 | Espresso + CarUiTestRule | 车载应用界面、多屏交互 |
| Service 层 | CarService 单元测试 | VHAL 属性读写、策略逻辑 |
| 系统层 | Tradefed + 车载测试用例 | 多屏切换、休眠唤醒、音频焦点 |
24.3.1 UI 层:Espresso + CarUiTestRule
Espresso 大家都熟,但车载 UI 测试有个特殊点:多屏。你需要在副屏上找一个按钮,然后在主屏上验证结果。
Google 提供了 CarUiTestRule,它可以让你指定测试的 Display ID。比如:
@Rule
public CarUiTestRule<MainActivity> rule = new CarUiTestRule<>(MainActivity.class)
.withDisplayId(Display.DEFAULT_DISPLAY); // 主屏
@Test
public void testClimateButtonOnMainScreen() {
onView(withText("空调")).perform(click());
onView(withId(R.id.climate_status)).check(matches(withText("已开启")));
}
如果要测副屏,把 withDisplayId 换成副屏的 ID 即可。我一般会在测试前先 dump 出所有 display 的信息:
adb shell dumpsys display | grep "DisplayDeviceInfo"
24.3.2 Service 层:VHAL 单元测试
VHAL 的逻辑往往很复杂,比如「车速超过 80 km/h 时自动关闭副屏视频」。这种逻辑最好用单元测试覆盖,而不是等到真车上再测。
我习惯用 Mockito + FakeVhal 来写:
@Test
public void testVideoPausedWhenSpeedExceeds80() {
// 模拟 VHAL 返回车速 85 km/h
when(mockVhal.get(VehicleProperty.SPEED)).thenReturn(85.0f);
// 触发策略检查
speedPolicy.onSpeedChanged(85.0f);
// 验证副屏视频被暂停
verify(mockSecondaryScreen).pauseVideo();
}
关键点:这种测试跑得飞快,几百个用例几秒钟就跑完了。我每次提交代码前都会跑一遍,确保 VHAL 策略没被改坏。
24.3.3 系统层:Tradefed 自动化
Tradefed 是 Android 官方的自动化测试框架,支持多设备、多屏、多模拟器。你可以写一个 XML 配置文件,定义测试步骤:
<configuration>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.example.car.tests" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
</test>
<device name="device1" />
</configuration>
然后通过命令行跑:
tradefed run config.xml
Tradefed 的好处是可以集成到 CI/CD 流水线里。我之前的团队就是每次合并代码后,自动在模拟器上跑一遍全量车载测试,大概 20 分钟出结果。
24.4 知识体系总览
下面这张图,我把本章的核心逻辑串起来了。你可以看到从「模拟器」到「VHAL 模拟」再到「自动化测试」的完整链路:
24.5 避坑指南与个人经验
最后,我分享几个实战中遇到的坑,希望能帮你少走弯路。
- 模拟器 GPU 渲染问题:有些车载动画(比如仪表盘指针旋转)在模拟器上会卡顿。我建议在模拟器配置里开启
hw.gpu.enabled=yes,并分配至少 2GB 显存。 - VHAL 属性权限:不是所有属性都能通过
dumpsys修改。比如VEHICLE_SPEED是只读的,你得用模拟器插件才能写。我一开始不知道,折腾了半天。 - 多屏测试的焦点问题:Espresso 默认只操作主屏。要操作副屏,必须用
CarUiTestRule指定 Display ID。否则点击事件会跑到主屏上,测试永远不过。 - Tradefed 超时设置:车载测试往往涉及等待动画、等待 VHAL 响应。我建议把单个用例的超时设为 30 秒,避免某个用例卡死导致整个测试套件挂掉。
一句话总结:模拟器 + VHAL 模拟 + 自动化测试,这三板斧能让你在没车的情况下,覆盖 80% 以上的测试场景。剩下的 20%,留给真车验证就够了。