Jetpack Compose ViewModel测试报错:cachedIn参数非空却为null
ViewModel测试中NullPointerException问题排查
报错信息
Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this> java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this>
测试类代码
@ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class MoviePopularViewModelTest { @Mock lateinit var getPopularMoviesUseCase: GetPopularMoviesUseCase private lateinit var moviePopularViewModel: MoviePopularViewModel private val pagingDataMovies = PagingData.from( listOf( Movie( id = 1, title = "Avengers", voteAverage = 7.1, imageUrl = "Url" ), Movie( id = 2, title = "Iron Men", voteAverage = 7.9, imageUrl = "Url" ) ) ) @Before fun setup() { moviePopularViewModel = MoviePopularViewModel(getPopularMoviesUseCase) } @Test fun `should validate the paging data object values when calling moviesPagingData`() = runTest { whenever(getPopularMoviesUseCase.invoke()).thenReturn( flowOf(pagingDataMovies) ) val pagingData = pagingDataMovies val result = moviePopularViewModel.uiState.results.first() result.map { resultMovie -> pagingData.map { pagingData -> assertThat(pagingData.title).isEqualTo(resultMovie.title) } } } }
ViewModel代码
@HiltViewModel class MoviePopularViewModel @Inject constructor( private val getPopularMoviesUseCase: GetPopularMoviesUseCase ) : ViewModel() { var uiState by mutableStateOf(MoviePopularState()) private set init { val movies = getPopularMoviesUseCase.invoke() .cachedIn(viewModelScope) uiState = uiState.copy(results = movies) } }
问题原因与修复方案
核心问题:初始化顺序错误
ViewModel的init块在setup()方法创建ViewModel实例时会立即执行,但此时测试方法还未设置getPopularMoviesUseCase的mock返回值。Mockito默认给mock方法返回null,导致getPopularMoviesUseCase.invoke()返回空值,调用cachedIn时,作为扩展函数接收者的Flow为null,触发非空参数的NullPointerException。
测试逻辑的额外问题
moviePopularViewModel.uiState.results.first()用法错误:results是Flow<PagingData<Movie>>,需通过collect收集数据,而非直接调用first()。- 对PagingData的
map操作不会直接执行,需通过收集行为触发数据加载。
修正步骤
- 调整mock时机:在创建ViewModel实例前,先设置好
getPopularMoviesUseCase的mock返回值,确保init块执行时能拿到有效Flow。 - 修正测试逻辑:正确收集Flow并验证PagingData内容。
修正后的测试类代码示例
@ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class MoviePopularViewModelTest { @Mock lateinit var getPopularMoviesUseCase: GetPopularMoviesUseCase private lateinit var moviePopularViewModel: MoviePopularViewModel private val pagingDataMovies = PagingData.from( listOf( Movie( id = 1, title = "Avengers", voteAverage = 7.1, imageUrl = "Url" ), Movie( id = 2, title = "Iron Men", voteAverage = 7.9, imageUrl = "Url" ) ) ) @Before fun setup() = runTest { // 创建ViewModel前先设置mock返回值 whenever(getPopularMoviesUseCase.invoke()).thenReturn(flowOf(pagingDataMovies)) moviePopularViewModel = MoviePopularViewModel(getPopularMoviesUseCase) } @Test fun `should validate the paging data object values when calling moviesPagingData`() = runTest { val collectedItems = mutableListOf<Movie>() // 收集Flow中的PagingData moviePopularViewModel.uiState.results.collect { pagingData -> pagingData.map { movie -> collectedItems.add(movie) } } // 验证收集到的数据是否符合预期 assertThat(collectedItems.size).isEqualTo(2) assertThat(collectedItems[0].title).isEqualTo("Avengers") assertThat(collectedItems[1].title).isEqualTo("Iron Men") } }
内容的提问来源于stack exchange,提问作者Daniel Lopes
相关产品推荐
相关产品推荐

