You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在单元测试中可靠测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 14:56:20