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 测试后台同步任务进度更新的完整流程:

TestListenableWorker 测试进度更新流程 1. 创建 TestListenableWorker 实例 2. 注册 progressObserver 3. 调用 worker.startWork() 4. setProgress() 多次更新 5. 获取 Result 并断言进度列表 说明: • progressObserver 是 LiveData,每次 setProgress() 都会发出 Data 对象。 • 断言应在 future.get() 之后进行,确保所有进度更新已提交。