单元测试NewsViewModel时cachedIn触发空指针异常,求解决方案
ViewModel单元测试中cachedIn引发NullPointerException的解决思路
异常信息
Exception in thread "UI thread @coroutine#1" java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this> at androidx.paging.CachedPagingDataKt.cachedIn(CachedPagingData.kt) at com.sarmad.newsprism.news.ui.NewsViewModel$getNewsStream$1.invokeSuspend(NewsViewModel.kt:46) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829) Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}@67526363, Dispatchers.Main] expected:<false> but was:<true> Expected :false Actual :true
核心问题分析
- Mock对象未设置返回值:测试中使用
@Mock注解的NewsRepository,但未给getBreakingNewsStream方法指定返回值,导致flatMapLatest操作后得到的Flow为null,调用cachedIn时触发空指针异常。 - 测试代码与ViewModel实现不匹配:测试中调用的
getNewsStream方法、依赖的newsFlow属性在NewsViewModel中均不存在,属于错误调用。 - ViewModel初始化参数缺失:
NewsViewModel构造函数需要NewsRepository和SavedStateHandle两个参数,但测试中仅传入了newsRepository,导致SavedStateHandle相关的流初始化逻辑异常。
相关代码实现
NewsViewModel.kt
package com.sarmad.newsprism.news.ui @HiltViewModel class NewsViewModel @Inject constructor( private val newsRepository: NewsRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { companion object { const val KEY_SUBREDDIT = "us" const val DEFAULT_SUBREDDIT = "androiddev" } init { if (!savedStateHandle.contains(KEY_SUBREDDIT)) { savedStateHandle[KEY_SUBREDDIT] = DEFAULT_SUBREDDIT } } @OptIn(ExperimentalCoroutinesApi::class) val articles = savedStateHandle.getLiveData<String>(KEY_SUBREDDIT) .asFlow() .flatMapLatest { newsRepository.getBreakingNewsStream(it) }.cachedIn(viewModelScope) private val _userMessage = MutableStateFlow<String?>(null) private val _isLoading = MutableStateFlow(false) private val _newsArticles = articles val uiState: StateFlow<NewsItemListUiState> = combine( _isLoading, _userMessage, _newsArticles ) { isLoading, userMessage, newsArticles -> NewsItemListUiState( news = newsArticles, isLoading = isLoading, userMessage = userMessage ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000L), initialValue = NewsItemListUiState(isLoading = true) ) fun refresh() { _isLoading.value = true viewModelScope.launch { newsRepository.refreshTasks() _isLoading.value = false } } }
NewsRepository.kt
package com.sarmad.newsprism.data.repository import androidx.paging.PagingData import com.sarmad.newsprism.data.entities.NewsResponse import kotlinx.coroutines.flow.Flow import com.sarmad.newsprism.data.Result import com.sarmad.newsprism.data.entities.Article interface NewsRepository { suspend fun getSearchedNewsStream(searchQuery: String, pageNumber: Int): Flow<NewsResponse> suspend fun getBreakingNewsStream(countryCode: String): Flow<PagingData<Article>> }
原测试代码 NewsViewModelTest.kt
package com.sarmad.newsprism.news.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.sarmad.newsprism.data.remotedatasource.api.NewsApi import com.sarmad.newsprism.data.repository.NewsRepository import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations internal class NewsViewModelTest { @Mock lateinit var newsRepository: NewsRepository @Mock private lateinit var newsApi: NewsApi private lateinit var newsViewModel: NewsViewModel @OptIn(DelicateCoroutinesApi::class) private val mainThreadSurrogate = newSingleThreadContext("UI thread") @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(mainThreadSurrogate) newsViewModel = NewsViewModel(newsRepository) } @OptIn(ExperimentalCoroutinesApi::class) @After fun tearDown() { Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher mainThreadSurrogate.close() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_getNewsFlow() = runTest { newsViewModel.getNewsStream("us") assertEquals(true, newsViewModel.newsFlow.value.isLoading) advanceUntilIdle() assertEquals( false, newsViewModel.newsFlow.value.isLoading ) assertNotNull( newsViewModel.newsFlow.value.news ) } }
解决方案
1. 修正ViewModel初始化
在测试中传入SavedStateHandle实例,确保构造参数完整:
// 在setUp方法中添加 val savedStateHandle = SavedStateHandle() savedStateHandle[NewsViewModel.KEY_SUBREDDIT] = "us" newsViewModel = NewsViewModel(newsRepository, savedStateHandle)
2. 为Mock Repository设置有效返回值
使用Mockito为getBreakingNewsStream方法返回空的PagingData流,避免Flow为null:
import androidx.paging.PagingData import kotlinx.coroutines.flow.flowOf // 在测试方法中添加 val emptyPagingData = PagingData.empty<Article>() `when`(newsRepository.getBreakingNewsStream(anyString())).thenReturn(flowOf(emptyPagingData))
3. 修正测试逻辑,观察正确的StateFlow
测试应观察ViewModel中实际存在的uiState,验证加载状态变化与数据更新:
@OptIn(ExperimentalCoroutinesApi::class) @Test fun test_uiStateChanges() = runTest { // 准备Mock数据 val emptyPagingData = PagingData.empty<Article>() `when`(newsRepository.getBreakingNewsStream("us")).thenReturn(flowOf(emptyPagingData)) `when`(newsRepository.refreshTasks()).thenReturn(Unit) // 收集uiState变化 val uiStates = mutableListOf<NewsItemListUiState>() val job = launch { newsViewModel.uiState.collect { uiStates.add(it) } } // 验证初始加载状态 assertEquals(true, uiStates.first().isLoading) // 触发刷新操作 newsViewModel.refresh() assertEquals(true, newsViewModel.uiState.value.isLoading) advanceUntilIdle() // 验证刷新完成后的状态 assertEquals(false, newsViewModel.uiState.value.isLoading) assertNotNull(newsViewModel.uiState.value.news) job.cancel() }
最终修正后的测试代码
package com.sarmad.newsprism.news.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.SavedStateHandle import androidx.paging.PagingData import com.sarmad.newsprism.data.entities.Article import com.sarmad.newsprism.data.repository.NewsRepository import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations internal class NewsViewModelTest { @Mock lateinit var newsRepository: NewsRepository private lateinit var newsViewModel: NewsViewModel private lateinit var savedStateHandle: SavedStateHandle @OptIn(DelicateCoroutinesApi::class) private val mainThreadSurrogate = newSingleThreadContext("UI thread") @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(mainThreadSurrogate) savedStateHandle = SavedStateHandle() savedStateHandle[NewsViewModel.KEY_SUBREDDIT] = "us" newsViewModel = NewsViewModel(newsRepository, savedStateHandle) } @OptIn(ExperimentalCoroutinesApi::class) @After fun tearDown() { Dispatchers.resetMain() mainThreadSurrogate.close() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_uiStateChanges() = runTest { // 准备Mock数据 val emptyPagingData = PagingData.empty<Article>() `when`(newsRepository.getBreakingNewsStream("us")).thenReturn(flowOf(emptyPagingData)) `when`(newsRepository.refreshTasks()).thenReturn(Unit) // 收集uiState变化 val uiStates = mutableListOf<NewsItemListUiState>() val job: Job = launch { newsViewModel.uiState.collect { uiStates.add(it) } } // 验证初始加载状态 assertEquals(true, uiStates.first().isLoading) // 触发刷新操作 newsViewModel.refresh() assertEquals(true, newsViewModel.uiState.value.isLoading) advanceUntilIdle() // 验证刷新完成后的状态 assertEquals(false, newsViewModel.uiState.value.isLoading) assertNotNull(newsViewModel.uiState.value.news) job.cancel() } }
内容的提问来源于stack exchange,提问作者sarmad sohaib
相关产品推荐
相关产品推荐

