Mock回放技术简介
先问大家一个问题:你写过 WorkManager 的单元测试吗?如果写过,大概都遇到过这样的尴尬——跑一次测试,等半天后台任务结果,好不容易跑完了,发现网络挂了或者数据库状态不对。我刚开始做 WorkManager 测试时,就掉进过这个坑里。后来才发现,其实有个很实用的方案——Mock 回放技术。今天这篇,我就带你搞清楚它到底是什么,以及为什么我们做 WorkManager 测试离不开它。
传统测试的痛点:为什么需要 Mock 回放?
直接看一个真实场景。假设你的 App 有一个数据同步 Worker,它要做三件事:
- 从网络 API 拉取最新数据
- 将数据存入本地数据库
- 更新 UI 通知用户
用普通方式写测试,Worker 执行时会真的发起网络请求。这时候问题就来了:
- 环境依赖:测试必须联网,而且后端接口不能挂。我在一次 CI 构建中遇到过 CI 服务器突然断网,整个测试套件全红了,排查了半天才发现不是代码的问题。
- 非确定性:后端返回的数据每次可能不同,导致断言结果不稳定。说白了,你今天测通过,明天可能就失败,但你根本改过代码。
- 耗时长:WorkManager 的任务可能有延迟约束(比如至少 15 分钟后才执行),你总不能真的等 15 分钟吧。
我曾经踩过的坑:有一次线上排查问题,发现 Worker 在特定网络延迟下会崩溃。我用模拟器复现,但模拟器的网络延迟和我线上实际环境差太多,始终复现不出来。后来换成 Mock 回放,把线上抓到的网络响应"回放"给测试代码,瞬间命中问题。
什么是 Mock 回放技术?
简单说,Mock 回放就是把真实运行环境(比如网络请求、数据库写入、ContentProvider 查询)的交互过程录下来,然后在测试运行时「回放」这些录制好的数据,替代真实的外部依赖。
你想想看,这和普通的 Mock(比如 Mockito)有什么不同?普通 Mock 是你手动写一段代码,规定某个方法返回某个值。而 Mock 回放更接近「录播」——你录制一次真实交互,以后测试时就播放它。好处是什么?
- 录制的数据是真实线上数据,更贴近问题场景
- 不需要手动写大量 Mock 代码,减少了维护成本
- 测试结果可复现,每次跑都一样
在实际项目中,常用的 Mock 回放工具有:
| 工具 | 适用场景 | 特点 |
|---|---|---|
| WireMock | 网络请求(HTTP/HTTPS) | 录制真实流量 & 回放;支持匹配规则和延迟模拟 |
| MockWebServer | 网络请求(OkHttp 生态) | 轻量级;适合本地单元测试 |
| Robolectric + Shadow | Android 组件(Database, WorkManager) | 模拟 Android 框架层;与 WorkManager TestHelper 配合使用 |
| 自定义录制器 | 数据库 / ContentProvider | 可以用拦截器(Interceptor)或 AOP 实现 |
核心概念:Mock 回放 = 录制 (Record) + 回放 (Replay)。录制阶段收集真实交互(请求 & 响应),回放阶段拦截相同请求并返回录制响应。测试代码不感知是否存在外部依赖。
为什么特别适合 WorkManager 测试?
WorkManager 本身设计为异步、可延迟、甚至可以跨进程执行。它的测试难点恰恰在于:
- 异步边界难以控制:Worker 的 doWork() 返回值需要等待,但等待时间不确定。
- 外部依赖多:Worker 内经常调用网络、数据库、文件 IO,这些都不适合在单元测试中真实执行。
- 状态可能被污染:多次测试间共享数据库、SharedPreferences,容易互相影响。
Mock 回放恰好能解决上面全部问题。举个例子:你用 WireMock 录制一次网络响应,然后让 Worker 在测试时走这个 Mock server,所有网络调用瞬间完成,而且数据完全可控。再加上 TestListenableWorkerBuilder 配合 WorkManagerTestInitHelper,你甚至可以精确控制 Worker 的执行生命周期。
// 使用 MockWebServer 录制并回放
@RunWith(RobolectricTestRunner::class)
class SyncWorkerTest {
private lateinit var mockServer: MockWebServer
@Before
fun setup() {
mockServer = MockWebServer()
mockServer.start()
// 假设之前已经录制好了 response_body.json
val recordedResponse = MockResponse()
.setResponseCode(200)
.setBody(readFromAssets("recorded/response_body.json"))
.addHeader("Content-Type", "application/json")
mockServer.enqueue(recordedResponse)
// 让 Worker 的 baseUrl 指向 mockServer
RetrofitClient.baseUrl = mockServer.url("/").toString()
}
@Test
fun testSyncWorker_returnsSuccess() {
val worker = TestListenableWorkerBuilder<SyncWorker>(context)
.setWorkerFactory(SyncWorker.Factory()) // 注入依赖
.build()
val result = worker.startWork().get() // 同步等待结果(测试环境下安全)
assertThat(result).isEqualTo(Result.success())
// 验证数据库或者其他状态
}
@After
fun teardown() {
mockServer.shutdown()
}
}
小技巧:录制回放数据时,我习惯将文件按「场景」分类保存到 assets/recorded/ 目录下。比如 response_body.json 对应正常数据,response_error.json 对应网络错误。这样测试用例的意图一目了然。
Mock 回放的核心流程
下面这张图概括了整个流程:从测试用例启动,到回放引擎拦截请求,再到断言验证。
从图中可以看到,整个流程非常透明:测试用例发起请求 → 回放引擎拦截 → 判断是否已录制 → 如果已录制直接返回,否则临时请求真实后端(仅用于录制)。这样,你在 CI 环境里可以完全离线跑测试,再也不用担心网络波动了。
什么时候使用 Mock 回放?什么时候该用普通 Mock?
这里我有一个判断经验,分享给你:
- 用 Mock 回放:当 Worker 依赖的外部响应是「数据型」的(比如 API 返回 JSON、数据库查询结果),而且这些数据在不同测试场景下需要保持一致的。尤其适合做回归测试、线上 BUG 复现。
- 用普通 Mock:当你要验证 Worker 内部的逻辑分支(比如某个 if/else),或者需要模拟极端异常(比如返回 null、抛特定异常)。普通 Mock 更灵活,但维护成本高。
我个人的习惯是:先把核心业务场景用 Mock 回放覆盖掉,保证每一次 CI 都稳定跑过;再针对边界条件用普通 Mock 做补充。这样既稳又快。
注意:Mock 回放的数据如果过期了(比如后端改了接口格式),测试会误判。我建议定期(比如每次大版本发布前)重新录制一次数据,或者结合契约测试来做校验。