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

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

问题根源梳理

  1. 类型完全不匹配:测试预期的是Result<Response<UpcomingResponse>>,但ViewModel的upcomingMovies LiveData存储的是UpcomingResponse(从Response的body()中提取的实体),两者类型不一致,这是核心错误。
  2. Mock数据缺失关键配置:你mock的Response对象没有设置body()的返回值,导致ViewModel中result.getOrNull()!!.body()为null,直接跳过了postValue的逻辑。
  3. 协程调度未同步:ViewModel使用viewModelScope默认绑定Dispatchers.Main,测试环境下如果没替换成测试调度器,协程可能在测试断言时还未执行完成,LiveData还没被赋值。
  4. 断言时机过早:即使协程调度正确,创建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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 17:05:30