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

Android ViewModel协程测试:launch未等待结果问题排查

Kotlin协程+Flow的ViewModel测试问题:测试未等待挂起函数结果完成

我使用Kotlin(1.6.21)、Coroutines(1.6.4)和Kotlin Flow进行ViewModel测试,遵循官方《Kotlin协程测试文档》,但ViewModel在测试完成前不会等待/返回挂起函数的结果。尝试了注入相同CoroutineDispatcher、传递相同CoroutineScope等所有热门方案,但均无效。以下是测试涉及的所有类代码,便于定位问题:

ReferEarnDetailViewModel.kt

注入了UseCase和CoroutineContextProvider,使用viewModelScope结合指定dispatcher调用API。但测试用例调用callReferEarnDetails()后,并未收集到mock用例方法发射的数据。尝试过直接调用仓库方法、不使用Kotlin Flow,结果同样失败。

@HiltViewModel
class ReferEarnDetailViewModel @Inject constructor(
    val appDatabase: AppDatabase?,
    private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
    private val coroutineContextProvider: CoroutineContextProvider
) : BaseViewModel() {

    fun callReferEarnDetails() {
        setProgress(true)
        viewModelScope.launch(coroutineContextProvider.default + handler) {
            referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
                .collect { referEarnDetail ->
                    parseReferEarnDetail(referEarnDetail)
                }
        }
    }

    private fun parseReferEarnDetail(referEarnDetail: ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
        when (referEarnDetail) {
            is ResultState.Success -> {
                setProgress(false)
                // 省略其他逻辑
            }
        }
    }
}

ReferEarnCodeUseCase.kt(返回API响应的Flow)

@ViewModelScoped
class ReferEarnCodeUseCase @Inject constructor(private val repository: IReferEarnRepository) : BaseUseCase {

    suspend fun execute(url: String): Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
        return repository.getReferralCode(url)
    }
}

CoroutineTestRule.kt

@ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher = StandardTestDispatcher()) : TestWatcher() {

    val testCoroutineDispatcher = object : CoroutineContextProvider {
        override val io: CoroutineDispatcher
            get() = testDispatcher
        override val default: CoroutineDispatcher
            get() = testDispatcher
        override val main: CoroutineDispatcher
            get() = testDispatcher
    }

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

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

ReferEarnDetailViewModelTest.kt

@RunWith(JUnit4::class)
@ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
    private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
    private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase

    @get:Rule
    val coroutineTestRule = CoroutineTestRule()
    @Mock
    lateinit var referEarnRepository: IReferEarnRepository
    @Mock
    lateinit var appDatabase: AppDatabase

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
        referEarnDetailViewModel = ReferEarnDetailViewModel(
            appDatabase,
            referEarnDetailsUseCase,
            coroutineTestRule.testCoroutineDispatcher
        )
    }

    @Test
    fun `test api response parsing`() = runTest {
        val data = ResultState.Success(TestResponse())

        // When
        Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
        // 调用ViewModel函数,该函数会进一步调用用例方法
        referEarnDetailViewModel.callReferEarnDetails()

        // API成功响应后此处应为false,但测试失败
        assertEquals(referEarnDetailViewModel.showProgress.get(), false)
    }
}

已尝试的解决方案

  • 测试启动viewModelScope协程的ViewModel函数的相关方案
  • 在ViewModel创建时注入并指定CoroutineScope的相关方案

内容的提问来源于stack exchange,提问作者Satwinder Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 23:15:41