第3章 后台任务模拟-协程:使用coroutineTestRule模拟异步延迟更新进度

3.1 测试原理

在Android UI测试中,后台任务(如网络请求、数据库操作)通常使用协程异步执行。直接测试带有delay或withContext的协程会导致测试超时或不可控。本章的核心原理是利用CoroutineTestRule将测试环境中的Dispatchers.Main替换为TestCoroutineDispatcher,从而让协程的异步行为变为同步可预测,并允许手动控制时间流逝(advanceTimeBy),实现模拟延迟更新进度条的效果。

3.2 环境搭建

在app/build.gradle中添加依赖:

androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"

创建自定义TestRule,封装TestCoroutineDispatcher和TestCoroutineScope:

class CoroutineTestRule : TestWatcher {
    val testDispatcher = TestCoroutineDispatcher()
    val testScope = TestCoroutineScope(testDispatcher)

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testScope.cleanupTestCoroutines()
    }
}

3.3 测试代码实现

3.3.1 待测ViewModel(简化版)

class ProgressViewModel : ViewModel() {
    private val _progress = MutableStateFlow(0)
    val progress: StateFlow = _progress

    fun startTask() {
        viewModelScope.launch {
            for (i in 1..100) {
                delay(50) // 模拟每50ms更新1%
                _progress.value = i
            }
        }
    }
}

3.3.2 测试代码

使用ComposeTestRule + CoroutineTestRule,通过advanceTimeBy控制时间:

@RunWith(AndroidJUnit4::class)
class ProgressViewModelTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @get:Rule
    val coroutineRule = CoroutineTestRule()

    @Test
    fun progressUpdatesAfterDelay() {
        val viewModel = ProgressViewModel()

        composeTestRule.setContent {
            val progress by viewModel.progress.collectAsState()
            Text(text = "Progress: $progress%")
        }

        viewModel.startTask()

        // 模拟前进250ms → 应更新5次(每50ms一次)
        coroutineRule.testDispatcher.advanceTimeBy(250L)
        composeTestRule.onNodeWithText("Progress: 5%").assertExists()

        // 模拟前进5000ms → 应达到100%
        coroutineRule.testDispatcher.advanceTimeBy(5000L)
        composeTestRule.onNodeWithText("Progress: 100%").assertExists()
    }
}

3.4 关键API与配置

API / 方法说明
TestCoroutineDispatcher可手动推进时间的调度器
advanceTimeBy(millis)快速跳过指定毫秒,执行期间的所有协程延时
runBlockingTest旧版测试协程的便捷扩展,但推荐使用advanceTimeBy控制粒度
Dispatchers.setMain替换主线程调度器为测试调度器

3.5 调试方法

  • 日志插入:在协程中添加Log.d("TEST", "progress=$i"),结合advanceTimeBy观察每次跳过的值。
  • 断点调试:在advanceTimeBy前后设置断点,检查testDispatcher.currentTime。currentTime反映虚拟时间。
  • 超时测试:故意让advanceTimeBy小于实际延迟,断言进度未达到预期,验证时序控制。

3.6 常见错误

常见错误1:忘记替换Dispatchers.Main,导致viewModelScope.launch仍使用真实主线程,测试异步不生效。
解决方案:在@Before或TestRule中调用Dispatchers.setMain(testDispatcher),并在结束时resetMain()。
常见错误2:使用runBlockingTest内嵌delay时,内部advanceTimeBy可能抛出UncompletedCoroutinesException。
推荐改用testScope.runBlockingTest + 显式advanceTimeBy控制,避免自动推进导致未完成协程被清理。
注意:viewModelScope默认使用Dispatchers.Main.immediate,需要确保替换的是Main调度器,否则协程仍可能异步执行。

3.7 性能与稳定性优化

  • 避免过量推进:每次advanceTimeBy的跨度不宜超过总延迟的10%,否则可能在单次推进中跳过多个状态更新,导致断言因并发时序而失败。推荐分批次推进(例如每500ms渐进)。
  • 复用TestCoroutineScope:在TestRule中使用单例TestCoroutineScope,并在finished中统一清理,防止泄漏测试协程。
  • 并行测试隔离:每个测试类使用独立的CoroutineTestRule实例,避免共享调度器状态导致测试间干扰。
  • 基准时间校准:在测试开始前通过testDispatcher.currentTime记录起点,断言时使用相对时间偏移,提高可维护性。

3.8 协程模拟进度更新流程图

协程模拟进度更新流程图(advanceTimeBy 控制) viewModel.startTask() for i in 1..100 { delay(50ms) } 协程挂起,等待时间推进 coroutineRule.testDispatcher advanceTimeBy(250L) StateFlow: progress = 5% 测试驱动: 手动跳过虚拟时间 代替真实 delay 关键检查: 展开 250ms 后 assertEquals(5%) 再展开 5000ms assertEquals(100%)