4. 后台任务模拟 — WorkManager:使用 TestListenableWorker 测试后台同步进度
4.1 测试原理
在 WorkManager 2.7+ 版本中,官方提供了 TestListenableWorker 类,用于对实现 ListenableWorker 的后台任务进行纯单元级别的测试。与集成测试不同,TestListenableWorker 不需要依赖 WorkManager 的完整运行时,也不会真正调度周期性或延迟任务,而是直接控制 Worker 的 doWork() 方法执行过程,并允许测试代码监听、验证任务过程以及进度(Progress)更新。
测试原理的核心在于:
- 脱离调度器 — 直接实例化 Worker,传入
Context和WorkerParameters(通过TestWorkerBuilder构建)。 - 进度可观测 — 注册
ForegroundInfo或通过setProgress()更新的进度数据依然通过ListenableFuture回传,测试可以订阅LiveData或Flow进行断言。 - 同步/异步可控 — 测试线程可以阻塞等待
doWork()完成,再验证最终结果与进度快照。
适用场景:当后台任务需要频繁向 UI 层报告同步百分比时(如备份文件、下载资源),使用
TestListenableWorker 可以快速验证进度更新的顺序、数值范围以及状态转换。
4.2 环境搭建
在模块级 build.gradle(或 build.gradle.kts)中添加以下依赖:
dependencies {
// WorkManager 核心库
implementation("androidx.work:work-runtime-ktx:2.9.0")
// WorkManager 测试支持库
androidTestImplementation("androidx.work:work-testing:2.9.0")
// 协程测试(如果 Worker 使用协程)
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
// 可选:JUnit 扩展 & Truth 断言
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("com.google.truth:truth:1.1.5")
}
确保 defaultConfig 中指定了 testInstrumentationRunner 为 androidx.test.runner.AndroidJUnitRunner。
版本注意:
work-testing 库的版本必须与 work-runtime-ktx 完全一致,否则可能出现 NoSuchMethodError 或进度回调不兼容问题。
4.3 测试代码实现
4.3.1 待测 Worker(已实现进度更新)
class SyncWorker(context: Context, params: WorkerParameters)
: ListenableWorker(context, params) {
override fun startWork(): ListenableFuture<Result> {
// 使用回调式进度更新(示例)
return CallbackToFutureAdapter.getFuture { completer ->
val total = 100
for (i in 0 until total) {
Thread.sleep(10) // 模拟耗时操作
setProgress(workDataOf("progress" to i + 1))
}
completer.set(Result.success())
object : CallbackToFutureAdapter.Cleanup {
override fun cancel() { /* 清理 */ }
}
}
}
}
4.3.2 测试类完整代码
@RunWith(AndroidJUnit4::class)
class SyncWorkerTest {
@get:Rule
val taskExecutorRule = WorkManagerTestInitHelper.getTaskExecutorRule()
@Test
fun testSyncProgress_updatesCorrectly() {
// 1. 创建 TestListenableWorker 实例
val context = ApplicationProvider.getApplicationContext<Context>()
val workerParams = TestWorkerBuilder.createWorkerParams()
val worker = SyncWorker(context, workerParams)
// 2. 构造 TestListenableWorker(实际就是同一个 worker 实例)
val testWorker = TestListenableWorker(worker)
// 3. 收集进度数据
val progressList = mutableListOf<Int>()
testWorker.progressObserver.observeForever { progressData ->
val p = progressData.getInt("progress", -1)
if (p != -1) progressList.add(p)
}
// 4. 执行 doWork
val future = testWorker.startWork()
// 5. 等待完成
ListenableFutureTask.create(future).run()
val result = future.get()
// 6. 断言结果与进度
assertThat(result).isEqualTo(Result.success())
assertThat(progressList).isNotEmpty()
assertThat(progressList.first()).isGreaterThan(0)
assertThat(progressList.last()).isEqualTo(100)
// 检查进度是否严格递增
for (i in 1 until progressList.size) {
assertThat(progressList[i]).isGreaterThan(progressList[i - 1])
}
}
}
4.3.3 关键 API 说明
| API | 用途 |
|---|---|
TestWorkerBuilder.createWorkerParams() | 生成一个模拟的 WorkerParameters,包含模拟的 id、输入数据等。 |
TestListenableWorker(worker) | 包装一个真实的 ListenableWorker,注入测试环境。 |
worker.progressObserver | 返回一个 LiveData<Data>,每次 setProgress() 调用都会触发更新。 |
worker.startWork() | 触发 doWork() 逻辑,返回 ListenableFuture<Result>。 |
ListenableFutureTask.create(future).run() | 在主线程同步等待 ListenableFuture 执行完成(测试专用工具)。 |
提示:对于协程版本的
CoroutineWorker,可以使用 TestCoroutineDispatcher 来精确控制延迟和时间,避免真实睡眠导致测试缓慢。但 TestListenableWorker 同样适用,只需将协程调度器替换为测试调度器。
4.4 调试方法
- 日志输出 — 在 Worker 的
doWork()中通过Log.d("SyncWorker", "progress=$p")输出中间值;测试用例中通过Log.w("Test", progressList.toString())查看收集到的进度序列。 - 断点调试 — 在
setProgress()调用处设置断点,观察progressObserver的回调是否被触发。 - WorkManager 测试规则 — 使用
WorkManagerTestInitHelper提供的TaskExecutorRule可以控制同步策略,确保测试线程不会因异步问题而卡死。 - 手动模拟失败 — 在测试中调用
worker.startWork()之前,可以通过worker.apply { }修改内部状态来模拟异常。
4.5 常见错误
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
progressObserver 始终为 null | 忘记调用 TestListenableWorker 的构造方法,或直接使用了 Worker 实例而非包装后的实例。 | 确保使用 val testWorker = TestListenableWorker(worker) 包装。 |
| 测试一直阻塞不返回 | ListenableFuture 没有在测试线程上得到调度;或者 Worker 内部调用了 Thread.sleep 导致时间不可控。 | 添加 TaskExecutorRule,并考虑用 Thread.sleep(0) 让出执行机会。 |
| 进度值缺失或顺序错乱 | progressObserver 注册时机晚于第一次 setProgress 调用。 | 在调用 startWork() 之前完成 observeForever 注册。 |
Result.success() 返回但 progressList 为空 | Worker 的 setProgress 在 completer.set() 之后才调用,但 completer.set() 会立即触发 future 完成。 | 确保所有进度更新在 completer.set() 之前执行。 |
4.6 性能与稳定性优化
- 避免真实 I/O — 在测试的 Worker 中不要执行真实的网络请求或文件读写;使用 mock 对象或注入 FakeRepository。
- 控制迭代粒度 — 如果 Worker 内部有循环更新进度(如每 1% 报告一次),测试中可将步长调大(例如每次更新 10%)以缩短执行时间,同时验证进度递增的逻辑。
- 使用超时保护 — 在
future.get(5, TimeUnit.SECONDS)上设置超时,防止因 Worker 死循环导致测试挂起。 - 共享测试规则 — 将
WorkManagerTestInitHelper.getTaskExecutorRule()抽取为测试基类的@Rule,避免重复初始化。 - 结果缓存 — 如果同一个 Worker 被多个测试用例调用,可以在
@Before中重置TestListenableWorker内部状态,避免前一次测试的进度残留影响后一次断言。
4.7 测试流程架构图
下图展示了使用 TestListenableWorker 测试后台同步任务进度更新的完整流程: