ViewModel单元测试预期成功但实际返回null问题求助
ViewModel测试失败:LiveData返回null的排查与解决
我想给ViewModel写个简单测试,验证它从Repository获取数据的逻辑。应用运行正常,但测试失败——预期拿到成功结果,实际得到null。我猜是ViewModel的init块没执行,因为这个块应该调用getUpcomingMovies()方法给upcomingMovies LiveData发值。下面是测试代码、ViewModel代码和测试结果,求帮忙排查:
测试代码
@ExperimentalCoroutinesApi class MoviesViewModelShould: BaseUnitTest() { private val repository: MoviesRepository = mock() private val upcomingMovies = mock<Response<UpcomingResponse>>() private val upcomingMoviesExpected = Result.success(upcomingMovies) @Test fun emitsUpcomingMoviesFromRepository() = runBlocking { val viewModel = mockSuccessfulCaseUpcomingMovies() assertEquals(upcomingMoviesExpected, viewModel.upcomingMovies.getValueForTest()) } private fun mockSuccessfulCaseUpcomingMovies(): MoviesViewModel { runBlocking { whenever(repository.getUpcomingMovies(1)).thenReturn( flow { emit(upcomingMoviesExpected) } ) } return MoviesViewModel(repository) } }
ViewModel代码
class MoviesViewModel( private val repository: MoviesRepository ): ViewModel() { val upcomingMovies: MutableLiveData<UpcomingResponse> = MutableLiveData() var upcomingMoviesPage = 0 private var upcomingMoviesResponse: UpcomingResponse? = null init { getUpcomingMovies() } fun getUpcomingMovies() = viewModelScope.launch { upcomingMoviesPage++ repository.getUpcomingMovies(upcomingMoviesPage).collect { result -> if (result.isSuccess) { result.getOrNull()!!.body()?.let { if (upcomingMoviesResponse == null) { upcomingMoviesResponse = it } else { val oldMovies = upcomingMoviesResponse?.results val newMovies = it.results oldMovies?.addAll(newMovies) } upcomingMovies.postValue(upcomingMoviesResponse ?: it) } } } } }
测试结果
expected:<Success(Mock for Response, hashCode: 1625939772)> but was:<null> Expected :Success(Mock for Response, hashCode: 1625939772) Actual :null
问题根源梳理
- 类型完全不匹配:测试预期的是
Result<Response<UpcomingResponse>>,但ViewModel的upcomingMoviesLiveData存储的是UpcomingResponse(从Response的body()中提取的实体),两者类型不一致,这是核心错误。 - Mock数据缺失关键配置:你mock的
Response对象没有设置body()的返回值,导致ViewModel中result.getOrNull()!!.body()为null,直接跳过了postValue的逻辑。 - 协程调度未同步:ViewModel使用
viewModelScope默认绑定Dispatchers.Main,测试环境下如果没替换成测试调度器,协程可能在测试断言时还未执行完成,LiveData还没被赋值。 - 断言时机过早:即使协程调度正确,创建ViewModel后立刻断言,协程可能还没完成数据收集和LiveData赋值操作。
修复方案
1. 修正预期值类型与Mock数据
新增mock的UpcomingResponse对象,并给Response设置body()返回值:
// 新增mock的UpcomingResponse实体 private val mockUpcomingResponse = mock<UpcomingResponse>() private val upcomingMovies = mock<Response<UpcomingResponse>>().apply { whenever(body()).thenReturn(mockUpcomingResponse) // 给Response设置有效的body } private val upcomingMoviesExpected = Result.success(upcomingMovies)
2. 配置测试用协程调度器
在BaseUnitTest中替换Dispatchers.Main为测试调度器,确保协程同步执行:
@ExperimentalCoroutinesApi open class BaseUnitTest { private val testDispatcher = UnconfinedTestDispatcher() @Before fun setup() { Dispatchers.setMain(testDispatcher) // 替换ViewModelScope的调度器,确保viewModelScope使用测试线程 val viewModel = ViewModel() viewModel.viewModelScope = CoroutineScope(testDispatcher + SupervisorJob()) } @After fun teardown() { Dispatchers.resetMain() } }
3. 等待协程执行完成后再断言
使用runBlockingTest配合advanceUntilIdle(),确保所有协程任务执行完毕后再验证结果:
@Test fun emitsUpcomingMoviesFromRepository() = runBlockingTest { val viewModel = mockSuccessfulCaseUpcomingMovies() advanceUntilIdle() // 等待所有协程任务完成 assertEquals(mockUpcomingResponse, viewModel.upcomingMovies.getValueForTest()) }
4. 确认getValueForTest()实现正确
确保扩展函数能正确获取LiveData的当前值:
fun <T> LiveData<T>.getValueForTest(): T? { var value: T? = null observeForever { value = it } return value }
内容的提问来源于stack exchange,提问作者Mert
相关产品推荐
相关产品推荐

