创建测试Worker:继承ListenableWorker、实现doWork、返回Result、异常处理

4.1 原理概述

WorkManager 单元测试的核心挑战在于:Worker 实例由 WorkManager 内部创建、调度和销毁,测试代码无法直接控制其生命周期。为了在不依赖真实调度器的情况下验证业务逻辑,我们需要创建一个测试专用 Worker。该 Worker 继承自 ListenableWorker,并显式暴露其核心行为:doWork() 的执行路径、Result 的返回以及异常处理策略。

通过测试 Worker,我们可以将异步任务简化为可预测的同步调用,从而在 JVM 单元测试中精确断言 Result.success()、Result.failure() 或 Result.retry() 的触发条件。同时,测试 Worker 充当了“探针”,允许我们注入 mock 依赖并验证异常恢复逻辑。

测试Worker 执行与结果断言流程 测试用例 测试Worker.doWork() 模拟后端 启动 网络/DB调用 返回 Result 数据/异常 Result.success() Result.failure() Result.retry() 断言:assertThat(worker.getResult()).isEqualTo(Result.success())

4.2 实现流程

创建测试 Worker 分为5个步骤,每个步骤都对应一个可度量的交付物:Worker 类定义、构造器注入、doWork 实现、Result 构建以及异常处理覆盖。

步骤操作关键代码/配置
1继承 ListenableWorkerclass TestFetchWorker(context, params) : ListenableWorker(context, params)
2定义模拟后端依赖(接口)private val backend: MockBackend
3实现 doWork:同步/异步转换return listenableFuture { ... }
4条件返回 Result.success / .failure / .retryif (success) Result.success() else Result.failure()
5捕获异常并转换为 Retrytry { ... } catch(e: Exception) { Result.retry() }

4.3 代码实现与 API 示例

以下是一个完整的测试 Worker 实现。注意:测试 Worker 不应持有 Android 框架依赖(如 Context 应使用 ApplicationProvider 或 Mock)。

// TestFetchWorker.kt
class TestFetchWorker(
    appContext: Context,
    params: WorkerParameters,
    private val backend: MockBackend = MockBackend()  // 默认实现,可在测试时替换
) : ListenableWorker(appContext, params) {

    private val result = SettableFuture<Result>()

    override fun doWork(): ListenableFuture<Result> {
        val inputUrl = inputData.getString("url") ?: ""
        
        try {
            val data = backend.fetch(inputUrl)
            if (data.isNotEmpty()) {
                result.set(Result.success())
            } else {
                result.set(Result.failure())
            }
        } catch (e: IOException) {
            // 网络错误应重试
            result.set(Result.retry())
        } catch (e: Exception) {
            // 其他未知错误直接失败
            result.set(Result.failure())
        }
        
        return result
    }
    
    // 测试辅助方法:获取结果
    fun getResult(): Result? {
        return result.getOrNull()
    }
}

4.4 Mock 策略与依赖注入

测试 Worker 必须支持依赖注入,以便在单元测试中替换真实后端。推荐使用构造器注入:

  • 将 MockBackend 定义为接口或抽象类,提供默认实现。
  • 在测试构造函数中传入 Mock 实例(如 Mockito.mock())。
  • 避免在 doWork 内部直接 new 对象。
最佳实践: 始终将 Worker 的协作者(如 Repository、ApiService)通过构造器传入。这不仅是测试友好设计,也是生产代码的推荐模式。

4.5 异常处理与 Result 映射

异常处理是单元验证的核心。你必须为每个可能的异常类型明确映射到对应的 Result 状态:

异常类型Result 映射业务语义
IOExceptionResult.retry()网络超时/连接失败,应重试
IllegalArgumentExceptionResult.failure()参数错误,无需重试
SecurityExceptionResult.failure()权限不足,需要用户介入
其他未预期异常Result.failure()防止无限重试

4.6 调试与验证

在编写测试前,先在 applicationId.debug 构建中运行 Worker,通过日志或 WorkManager.getWorkInfoByIdLiveData() 观察 Result 的实际输出。推荐在 Worker 内增加调试日志:

if (BuildConfig.DEBUG) {
    Log.d("TestWorker", "doWork called, url=$inputUrl, result=${result.get()}")
}

4.7 常见错误与稳定性优化

  • 忽略 ListenableWorker 的线程模型: 不要在 doWork() 中直接阻塞主线程。必须使用 ListenableFuture 或 suspend 函数。
  • Result 返回静默失败: 所有异常路径都必须有明确的 Result 返回。遗漏会导致 WorkManager 无限重试或 Worker 卡死。
  • 测试 Worker 直接依赖 Android 类: 使用 Shadow 或 Robolectric 时仍可能引入环境差异。优先用纯 JVM 可替换的接口。
  • SettableFuture 未正确设置: 每个返回路径确保 result.set() 被调用,否则测试将超时。
注意: 测试 Worker 不应在生产代码中直接使用。它只用于单元测试环境的模拟和断言。生产 Worker 应当使用 CoroutineWorker 或 RxWorker 以获得更好的生命周期管理。

4.8 交付物清单

  • TestFetchWorker.kt — 完整的 ListenableWorker 实现,包含异常处理和 Result 映射。
  • MockBackend.kt — 可被 Mock 的后端接口及默认实现。
  • TestFetchWorkerTest.kt — JUnit 测试类(下一章深入)