车载应用测试:从单元测试到实车验证的完整链路
测试这件事,说实话在车载开发里经常被低估。我见过不少团队,代码写得飞起,一到实车就各种崩溃。为什么?因为车载环境太特殊了——屏幕尺寸、硬件性能、系统权限,跟手机完全不是一个量级。
今天我们就来聊聊车载应用测试的四个层次:单元测试、UI自动化、模拟器测试、实车测试。这四层不是选做,而是必须层层递进。
一、单元测试:代码的第一道防线
单元测试,说白了就是验证你写的每个函数、每个方法对不对。我个人的习惯是,写业务代码之前先把测试接口定义好——这叫测试驱动开发(TDD)。虽然一开始会慢一点,但后期改 bug 的时间能省一大半。
1.1 JUnit 5 基础用法
JUnit 5 是目前 Android 项目的标配。跟老版本比,它最大的改进是支持了 Lambda 表达式和更灵活的断言。
// 一个典型的 ViewModel 单元测试
class MediaViewModelTest {
private lateinit var viewModel: MediaViewModel
private lateinit var mockRepository: MediaRepository
@BeforeEach
fun setUp() {
// 我习惯在 setUp 里初始化所有依赖
mockRepository = mock(MediaRepository::class.java)
viewModel = MediaViewModel(mockRepository)
}
@Test
fun `当播放列表为空时,状态应为 EMPTY`() {
`when`(mockRepository.getPlaylist()).thenReturn(emptyList())
viewModel.loadPlaylist()
assertEquals(UIState.EMPTY, viewModel.uiState.value)
}
@Test
fun `播放列表加载失败时,应显示错误信息`() {
`when`(mockRepository.getPlaylist()).thenThrow(NetworkException("超时"))
viewModel.loadPlaylist()
assertTrue(viewModel.uiState.value is UIState.Error)
assertEquals("超时", (viewModel.uiState.value as UIState.Error).message)
}
}
1.2 Mockito 模拟依赖
车载应用里,很多依赖是没法在单元测试里真实运行的——比如 GPS 模块、CAN 总线通信。这时候就需要 Mockito 上场了。
我曾经遇到过一个坑:某个定位服务在模拟器上跑得好好的,一上实车就报空指针。后来发现是 mock 的时候忘了模拟 onLocationChanged 回调。从那以后,我写 mock 都会列一个清单,把所有回调路径都覆盖到。
// Mockito 模拟车载定位服务
@Test
fun `定位更新时,车速应同步更新`() {
val mockLocationService = mock(LocationService::class.java)
val speedObserver = mock(Observer::class.java)
// 模拟位置更新
val mockLocation = mock(Location::class.java)
`when`(mockLocation.speed).thenReturn(60.0f)
// 触发回调
mockLocationService.onLocationChanged(mockLocation)
// 验证车速是否更新
verify(speedObserver).onChanged(60.0f)
}
open 关键字,或者用 MockK 替代。
二、UI 自动化测试:用 Espresso 模拟用户操作
单元测试保证逻辑正确,但界面交互对不对?按钮点没点到位?这些就得靠 UI 自动化测试了。
Espresso 是 Google 官方的 UI 测试框架,它最大的优点是「自动等待」——你不用手动写 Thread.sleep(),它会等界面渲染完成再执行下一步操作。
2.1 Espresso 核心用法
@RunWith(AndroidJUnit4::class)
class MediaPlayerActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MediaPlayerActivity::class.java)
@Test
fun `点击播放按钮后,应显示暂停图标`() {
// 找到播放按钮并点击
onView(withId(R.id.btn_play))
.check(matches(withText("播放")))
.perform(click())
// 验证按钮文字变为暂停
onView(withId(R.id.btn_play))
.check(matches(withText("暂停")))
}
@Test
fun `进度条拖动到50%时,播放位置应更新`() {
// 模拟拖动 SeekBar
onView(withId(R.id.seek_bar))
.perform(setProgress(50))
// 验证当前播放时间
onView(withId(R.id.tv_current_time))
.check(matches(withText("01:30")))
}
}
2.2 车载 UI 测试的特殊之处
车载屏幕跟手机不一样——分辨率通常是 1280x720 或 1920x1080,而且很多操作依赖旋钮而不是触摸。这时候 Espresso 的 onView 可能就不够用了。
我建议配合 UI Automator 一起使用。UI Automator 可以跨应用操作,比如模拟旋钮的旋转事件。
// 模拟旋钮向右旋转
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.pressDPadRight() // 模拟旋钮右转
device.pressDPadCenter() // 模拟按下确认
三、车载模拟器测试:低成本的环境验证
说实话,不是每个团队都有条件随时用实车测试。这时候模拟器就是最好的替代方案。
Android Automotive 官方提供了 DHU(Desktop Head Unit)模拟器,它可以在电脑上模拟车载中控屏的环境。你想想看,不用上车就能调试界面,多方便。
3.1 配置 DHU 模拟器
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 下载 Automotive 系统镜像 | 在 Android Studio 的 SDK Manager 里选择 "Android Automotive OS" |
| 2 | 创建 AVD | 选择 Automotive 设备类型,分辨率建议 1280x720 |
| 3 | 启动模拟器 | 用命令行 emulator -avd my_avd -netdelay none -netspeed full |
| 4 | 安装应用 | 直接拖拽 APK 到模拟器窗口,或 adb install |
3.2 模拟器测试 Checklist
- 分辨率适配:检查 UI 在不同分辨率下是否变形
- 横竖屏切换:车载屏幕基本都是横屏,但也要测试竖屏模式
- 多窗口模式:模拟分屏显示,比如导航+音乐同时运行
- 系统权限:测试定位、麦克风等权限的申请流程
- 网络切换:模拟 4G/5G 和离线状态
四、实车测试流程:最后的把关
终于到了实车测试这一步。说实话,这是最累但也是最有成就感的一环。你想想看,自己写的代码在真实的车上跑起来,那种感觉跟模拟器完全不一样。
4.1 实车测试前的准备
我一般会准备一个测试清单,按优先级排列:
- 基础功能验证:应用能否正常启动、退出、切换
- 交互体验:触摸响应是否灵敏,旋钮操作是否流畅
- 性能监控:CPU、内存、GPU 占用率是否在合理范围
- 异常场景:断网、GPS 信号丢失、系统休眠唤醒
- 兼容性:不同车型、不同系统版本
4.2 实车测试的坑
我曾经在一次实车测试中翻过车——应用在模拟器上跑了一个月都没问题,结果上车就闪退。查了半天,发现是车载系统的 SurfaceFlinger 版本跟模拟器不一样,导致某些渲染 API 不兼容。
从那以后,我养成了一个习惯:实车测试前,先跑一遍 CTS(兼容性测试套件)。虽然 CTS 主要是给系统厂商用的,但跑一下能提前发现很多底层兼容性问题。
4.3 测试报告与回归
每次实车测试后,我都会整理一份测试报告,包含:
- 测试环境(车型、系统版本、硬件配置)
- 测试用例执行情况(通过/失败/阻塞)
- Bug 列表及严重等级
- 性能数据(帧率、内存峰值、启动时间)
修复完 bug 后,一定要做回归测试。我个人的标准是:单元测试全部通过 → UI 测试覆盖修改的页面 → 模拟器跑一遍完整流程 → 实车验证修复的 bug。少一步都不行。
总结
车载应用测试不是一锤子买卖,而是一个持续的过程。从单元测试到实车验证,每一层都有它不可替代的价值。
单元测试帮你守住代码质量的下限,UI 测试保证交互不出错,模拟器测试降低环境适配成本,实车测试做最终验收。四层叠加,才能做出真正可靠的车载应用。
最后说一句:测试不是为了找茬,而是为了让你睡得安稳。代码写得再好,不上车跑一遍,你永远不知道它会不会在关键时刻掉链子。
公众号:蓝海资料掘金营,微信deep3321