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 里。

我简单列一下步骤:

  1. 创建一个 Android Library 模块,依赖 android.carandroid.car.hal
  2. 实现 VehicleHal 抽象类,重写 getAllPropertiessetget 等方法。
  3. onCreate 里启动一个定时器,定期更新属性值(比如每 100ms 更新一次车速)。
  4. 编译成 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 模拟」再到「自动化测试」的完整链路:

车载测试与仿真知识体系 Android Automotive 模拟器 VHAL 模拟:MockVhal / 自定义插件 UI 层:Espresso Service 层:单元测试 系统层:Tradefed 持续集成 / 快速反馈

24.5 避坑指南与个人经验

最后,我分享几个实战中遇到的坑,希望能帮你少走弯路。

  • 模拟器 GPU 渲染问题:有些车载动画(比如仪表盘指针旋转)在模拟器上会卡顿。我建议在模拟器配置里开启 hw.gpu.enabled=yes,并分配至少 2GB 显存。
  • VHAL 属性权限:不是所有属性都能通过 dumpsys 修改。比如 VEHICLE_SPEED 是只读的,你得用模拟器插件才能写。我一开始不知道,折腾了半天。
  • 多屏测试的焦点问题:Espresso 默认只操作主屏。要操作副屏,必须用 CarUiTestRule 指定 Display ID。否则点击事件会跑到主屏上,测试永远不过。
  • Tradefed 超时设置:车载测试往往涉及等待动画、等待 VHAL 响应。我建议把单个用例的超时设为 30 秒,避免某个用例卡死导致整个测试套件挂掉。

一句话总结:模拟器 + VHAL 模拟 + 自动化测试,这三板斧能让你在没车的情况下,覆盖 80% 以上的测试场景。剩下的 20%,留给真车验证就够了。

公众号:蓝海资料掘金营,微信deep3321