第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记录起点,断言时使用相对时间偏移,提高可维护性。