ViewModel协程测试中LiveData预期MyState.Success却返回null
问题描述
在ViewModel单元测试中,调用interactor("AnyInteractor")后,断言LiveData的state值应为MyState.Success,但实际返回null,测试失败:
预期值:com.br.testesunitarios.ui.MyState$Success@62fad19
实际值:null
疑问
- 是否是LiveData异步发射值导致了这种不匹配?
- 是否需要采用不同的测试策略来处理协程中的LiveData更新?
- 如何确保测试等待到最终的
MyState.Success状态被发射?
ViewModel实现
package com.br.testesunitarios.ui.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.br.testesunitarios.ui.MyState import com.br.testesunitarios.ui.ProviderUseCase import kotlinx.coroutines.delay import kotlinx.coroutines.launch class HomeViewModel : ViewModel() { private val _state: MutableLiveData<MyState> = MutableLiveData() val state: LiveData<MyState> = _state fun interactor(interactor: String) { when (interactor) { "AnyInteractor" -> { executarMetodo() } } } private fun executarMetodo() { viewModelScope.launch { try { _state.value = MyState.Loading delay(1000) _state.value = MyState.Success delay(1000) when (ProviderUseCase().execute()) { MyState.Success -> { _state.value = MyState.Success } else -> { _state.value = MyState.Failure } } } catch (e: Exception) { _state.value = MyState.Failure } } } }
测试用例
package com.br.testesunitarios.ui.home import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.br.testesunitarios.ui.MyState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.mockito.junit.MockitoJUnitRunner import org.junit.runner.RunWith @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class HomeViewModelTest { val instantExecutorRule = InstantTaskExecutorRule() private val testDispatcher = StandardTestDispatcher() @BeforeEach fun setUp() { Dispatchers.setMain(testDispatcher) } @AfterEach fun tearDown() { Dispatchers.resetMain() } @Test fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest { val viewModel = HomeViewModel() viewModel.interactor("AnyInteractor") assertEquals(MyState.Success, viewModel.state.value) // 此行测试失败 } }
问题分析与解决方案
1. 核心问题拆解
测试失败的关键原因有两个:
- 协程任务未被触发:使用
StandardTestDispatcher时,delay等挂起函数不会自动执行,测试代码直接断言时,协程逻辑还没跑完 - LiveData更新未被同步捕获:LiveData的
value获取是即时的,而ViewModel中的状态更新是异步的,直接读取会拿到初始的null值 - 规则未生效:
InstantTaskExecutorRule未加@get:Rule注解,导致LiveData无法在测试中同步执行
2. 针对性解决方案
方案一:修复测试逻辑,等待协程与LiveData更新
修改测试用例,解决三个问题:启用InstantTaskExecutorRule、推进协程调度器、捕获LiveData的最终状态:
@ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class HomeViewModelTest { @get:Rule // 必须加此注解才能启用规则 val instantExecutorRule = InstantTaskExecutorRule() private val testDispatcher = StandardTestDispatcher() @BeforeEach fun setUp() { Dispatchers.setMain(testDispatcher) // Mock ProviderUseCase,确保返回预期的Success mockStatic(ProviderUseCase::class.java).use { mocked -> mocked.`when`<MyState> { ProviderUseCase().execute() } .thenReturn(MyState.Success) } } @AfterEach fun tearDown() { Dispatchers.resetMain() } @Test fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest { val viewModel = HomeViewModel() var capturedState: MyState? = null // 观察LiveData,捕获所有状态更新 viewModel.state.observeForever { state -> capturedState = state } viewModel.interactor("AnyInteractor") // 推进调度器,让所有协程任务(包括两次delay)执行完毕 testDispatcher.scheduler.advanceUntilIdle() assertEquals(MyState.Success, capturedState) } }
方案二:用工具类简化LiveData测试
可以封装一个工具类,统一处理LiveData的等待逻辑:
import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit object LiveDataTestUtil { fun <T> LiveData<T>.getOrAwaitValue(timeout: Long = 2): T { var data: T? = null val latch = CountDownLatch(1) val observer = Observer<T> { value -> data = value latch.countDown() } observeForever(observer) // 等待状态更新,超时则抛出异常 latch.await(timeout, TimeUnit.SECONDS) removeObserver(observer) @Suppress("UNCHECKED_CAST") return data as T } }
使用工具类后的测试代码更简洁:
@Test fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest { val viewModel = HomeViewModel() viewModel.interactor("AnyInteractor") testDispatcher.scheduler.advanceUntilIdle() assertEquals(MyState.Success, viewModel.state.getOrAwaitValue()) }
3. ViewModel优化建议
为了提升可测试性,建议调整ViewModel的依赖注入方式:
class HomeViewModel( // 通过构造函数注入依赖,测试时可传入Mock实例 private val providerUseCase: ProviderUseCase = ProviderUseCase() ) : ViewModel() { private val _state: MutableLiveData<MyState> = MutableLiveData() val state: LiveData<MyState> = _state fun interactor(interactor: String) { when (interactor) { "AnyInteractor" -> executarMetodo() } } private fun executarMetodo() { viewModelScope.launch { try { _state.value = MyState.Loading delay(1000) // 合并重复的状态设置逻辑 val useCaseResult = providerUseCase.execute() _state.value = if (useCaseResult is MyState.Success) MyState.Success else MyState.Failure } catch (e: Exception) { _state.value = MyState.Failure } } } }
此时测试可直接传入Mock的UseCase,无需静态Mock:
@Test fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest { val mockUseCase = mock(ProviderUseCase::class.java) `when`(mockUseCase.execute()).thenReturn(MyState.Success) val viewModel = HomeViewModel(mockUseCase) var capturedState: MyState? = null viewModel.state.observeForever { capturedState = it } viewModel.interactor("AnyInteractor") testDispatcher.scheduler.advanceUntilIdle() assertEquals(MyState.Success, capturedState) }
内容的提问来源于stack exchange,提问作者Kleyton Prestes
相关产品推荐
相关产品推荐

