Mock回放概述
各位,我们直接切入正题。Mock回放这个词,你要是第一次听到,可能会想:这跟Mockito那种模拟数据有啥区别?说白了,它是一种把真实交互录下来,再一模一样的回放的技术。我最早接触这东西是在做动态化UI的时候,组件状态太多,手动构造数据根本搞不过来,后来发现Mock回放简直就是救星。
什么是Mock回放
简单理解:录下来,再放一遍。
在Compose UI测试场景里,我们通常需要验证某个组件在收到一系列用户事件、异步回调、状态变化后,UI是否正确。传统做法是手动写一大堆 `state` 和 `event`,费时费力。Mock回放的做法是:先用真机或模拟器跑一次真实交互,把所有的输入事件、网络响应、状态变更全部录制成一段“磁带”;然后在测试环境中“播放”这段磁带,让组件以为它又经历了一遍同样的流程。
核心要素:
- 记录器—— 捕获所有与UI相关的输入(点击、滑动、输入框文本)和输出(状态快照、渲染树)
- 序列化格式—— 把记录的数据存为JSON或二进制文件,可版本控制
- 回放引擎—— 在测试运行时,驱动Compose的 `state`、`LaunchedEffect`、`snapshotFlow` 等API按记录的顺序执行
为什么需要Mock回放
你想想看,一个复杂的Compose页面,可能包含几十个可组合项、网络请求、动画、键盘弹出、横竖屏切换……你要用传统单元测试覆盖每一种组合?我试过,光写一个页面的状态组合测试,就花了整整两周,最后还漏了一个边缘情况导致线上闪退。
Mock回放解决的核心痛点:
- 重复造轮子 —— 真实交互场景已经在开发过程中跑过了,何必再手动模拟一遍?
- 难以重现的bug —— 用户反馈某个操作后UI崩了,但测试环境死活复现不了。有了录制回放,直接让用户提供一次录制文件即可。
- 回归成本高 —— 每次改动UI,原有的手工测试用例可能作废。但Mock回放只要重新录一次,测试就自动更新。
- 协作效率 —— 产品、QA、开发共用同一份录制数据,减少沟通偏差。
| 问题 | 传统方式 | Mock回放 |
|---|---|---|
| 测试编写时间 | 需手动模拟所有状态 | 一次录制,多次使用 |
| 维护成本 | UI变化后手工重写测试 | 重录即可 |
| 覆盖完整交互 | 容易遗漏复杂流程 | 完全忠实于真实操作 |
| 协作偏差 | 各方理解可能不同 | 同一份数据说话 |
与传统测试的区别
这里我得说一句,Mock回放不是要取代传统测试,它是补充。我个人的习惯是:单元测试和集成测试继续用,Mock回放专门用来覆盖用户端到端的主流程和复杂交互。
关键差异点:
- 数据来源:传统测试靠程序员手动构造输入;Mock回放靠真实操作录制
- 确定性:传统测试每次运行结果理论上相同;Mock回放保证完全一致(只要录制和回放环境一致)
- 维护方式:传统测试用例随UI变更手动调整;Mock回放只需要重新录制一次
- 调试能力:传统测试定位问题需要加断点;Mock回放可以直接回放到出错的步骤,并打印出当时的所有状态
注意:Mock回放不适合所有场景。比如对时间精度要求极高的动画、随机数、传感器数据,录制回放可能产生偏差。我曾经遇到过因为设备屏幕尺寸不同,录制时的触摸坐标在回放时偏移导致误触,后来加了归一化坐标才解决。
// 一个简单的Mock回放使用示意(伪代码)
val recorder = UiRecorder()
recorder.startRecording(composableUnderTest)
// 用户在真机上点击了按钮
click(button)
// 录制结束,保存为文件
val replayData = recorder.stopRecording()
saveToFile("test_case_001.json")
// 在测试中回放
val player = UiPlayer()
player.load(replayData)
player.play() // 驱动Compose重现所有交互和状态变化
assertEquals(expectedState, actualState)
为了让你更直观的理解整个体系,我画了张图:
回到开头的问题:为什么要用Mock回放?因为我在真实项目中吃过亏。有一次产品提了个需求,要在电商列表里加一个“滑动删除”的交互,我手写了十几个测试用例,结果上线后还是有滑出边界的情况。后来我用录制回放把真实用户的滑动轨迹录下来,直接作为测试数据,一次性找到了所有边界问题。从那以后,凡是涉及多点触控、嵌套滑动、手势冲突的测试,我都首选Mock回放。