如何在单元测试中可靠测试Dispatcher IO与ViewModelScope异步逻辑?
问题场景
测试ViewModel时,需要等待两个基于Dispatchers.IO的异步网络调用完成后再执行断言,但当前测试中runCurrent()无法等待这些任务,导致断言失败。以下是简化代码:
ViewModel代码
fun initialise() { viewModelScope.launch(coroutineExceptionHandler) { val accountDeferred = async(Dispatchers.IO) { accountModel.getAccounts() } val contentDeferred = async(Dispatchers.IO) { contentModel.getContent() } val account = accountDeferred.await() val content = contentDeferred.await() handleResult(account, content) // 更新viewState } }
测试代码(简化版)
class Test { @get:Rule val coroutineRule = CoroutineTestRule(StandardTestDispatcher()) @Test fun `测试初始化逻辑`() { runTest{ whenever(accountModel.getAccounts()).thenReturn( Result.success(getAccountContent(), ReasonStatus.empty())) whenever(contentModel.getContent()).thenReturn( Result.success(getContent(), ReasonStatus.empty())) // 初始化ViewModel viewmodel = ViewModel(accountModel, contentModel) runCurrent() // 调试时发现viewState在两个异步任务完成前就被读取 val viewState = viewmodel.viewState.value // 断言失败,因为网络调用未完成 assertNotEmpty(viewState.accounts) } } }
可靠解决方案
方法1:依赖注入Dispatcher,避免硬编码Dispatchers.IO
核心思路是让ViewModel的Dispatcher可配置,测试时传入测试调度器,确保所有协程都在测试调度器中执行,这样runCurrent()或advanceUntilIdle()能控制任务执行。
修改ViewModel:
class MyViewModel( private val accountModel: AccountModel, private val contentModel: ContentModel, // 注入IO Dispatcher,默认用真实IO线程 private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : ViewModel() { fun initialise() { viewModelScope.launch(coroutineExceptionHandler) { // 使用注入的dispatcher代替硬编码的Dispatchers.IO val accountDeferred = async(ioDispatcher) { accountModel.getAccounts() } val contentDeferred = async(ioDispatcher) { contentModel.getContent() } val account = accountDeferred.await() val content = contentDeferred.await() handleResult(account, content) } } }
修改测试代码:
@Test fun `测试初始化逻辑`() = runTest { // 配置mock返回值 whenever(accountModel.getAccounts()).thenReturn( Result.success(getAccountContent(), ReasonStatus.empty())) whenever(contentModel.getContent()).thenReturn( Result.success(getContent(), ReasonStatus.empty())) // 传入测试调度器到ViewModel val testDispatcher = StandardTestDispatcher(testScheduler) viewmodel = MyViewModel(accountModel, contentModel, testDispatcher) viewmodel.initialise() runCurrent() // 现在会执行所有测试调度器中的任务,包括两个async调用 val viewState = viewmodel.viewState.value assertNotEmpty(viewState.accounts) // 断言成功 }
方法2:使用advanceUntilIdle()等待所有协程完成
如果已经确保所有Dispatcher都被替换为测试调度器(比如通过依赖注入),可以用advanceUntilIdle()代替runCurrent()。它会自动推进时间,直到所有挂起的协程任务都执行完毕。
测试代码修改:
@Test fun `测试初始化逻辑`() = runTest { // mock配置... val testDispatcher = StandardTestDispatcher(testScheduler) viewmodel = MyViewModel(accountModel, contentModel, testDispatcher) viewmodel.initialise() advanceUntilIdle() // 等待所有协程任务完成,包括两个async的await val viewState = viewmodel.viewState.value assertNotEmpty(viewState.accounts) }
方法3:全局替换Dispatchers.IO为测试调度器(应急方案)
如果无法修改ViewModel代码,可以在测试中全局替换Dispatchers.IO为测试调度器。注意这种方式会影响所有测试用例,需要在@After中重置。
测试代码修改:
class Test { @get:Rule val coroutineRule = CoroutineTestRule(StandardTestDispatcher()) private lateinit var originalIoDispatcher: CoroutineDispatcher @Before fun setup() { originalIoDispatcher = Dispatchers.IO // 全局替换IO调度器为测试调度器 @OptIn(ExperimentalCoroutinesApi::class) Dispatchers.IO = coroutineRule.dispatcher } @After fun teardown() { // 重置IO调度器 @OptIn(ExperimentalCoroutinesApi::class) Dispatchers.IO = originalIoDispatcher } @Test fun `测试初始化逻辑`() = runTest { // mock配置... viewmodel = ViewModel(accountModel, contentModel) viewmodel.initialise() advanceUntilIdle() val viewState = viewmodel.viewState.value assertNotEmpty(viewState.accounts) } }
内容的提问来源于stack exchange,提问作者keyboard_user
相关产品推荐
相关产品推荐

