ViewModel单元测试疑问:为何需延迟?测试目录是否正确?
嘿,作为经常写ViewModel单元测试的人,这俩问题我太熟了!咱们一个个说清楚:
问题1:为什么需要delay(10)?怎么优化?
为什么需要延迟?
你遇到的本质是协程调度与时序问题:虽然你用了InstantTaskExecutorRule解决了LiveData的主线程同步问题,但ViewModel里调用repository.getBeerStyles()的协程逻辑,和测试代码里的verify调用之间存在调度差——测试代码的verify会在协程还没来得及触发repository调用时就执行,所以才会报“零交互”的错误。delay(10)是靠强行等待让协程完成调度,属于临时hack,不是可靠的解法。
最优实现方案
核心思路是用协程测试工具让异步逻辑同步执行,消除时序差,具体可以这么改:
- 替换
runBlocking为runBlockingTest(旧版协程)或TestScope(新版协程),它能让挂起函数立即执行,不需要等待。 - 用
advanceUntilIdle()代替delay,确保所有挂起的协程任务都完成后再执行验证。 - 移除不必要的
runBlocking包裹(mock挂起函数时不需要额外包裹)。
修改后的测试代码示例:
@RunWith(JUnit4::class) @ExperimentalCoroutinesApi class BeerStylesViewModelTest { private lateinit var viewModel: BeerStylesViewModel private lateinit var repository: BreweryRepository private lateinit var beerStylesObserver: Observer<Resource<List<StylesData>>> private val successResource = Resource.success(mockedBeerStylesList) @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() // 新版协程推荐用TestDispatcher替代自定义线程 private val testDispatcher = UnconfinedTestDispatcher() @Before fun setUp() { Dispatchers.setMain(testDispatcher) repository = mock() // 挂起函数mock不需要runBlocking包裹 whenever(repository.getBeerStyles()).thenReturn(successResource) viewModel = BeerStylesViewModel(repository) beerStylesObserver = mock() } @Test fun `when load beerStyles success`() = runBlockingTest { viewModel.beerStyles.observeForever(beerStylesObserver) // 让所有挂起的协程任务执行完成 advanceUntilIdle() verify(repository).getBeerStyles() verify(beerStylesObserver).onChanged(Resource.loading(null)) verify(beerStylesObserver).onChanged(successResource) // 别忘了移除观察者,避免测试内存泄漏 viewModel.beerStyles.removeObserver(beerStylesObserver) } @After fun tearDown() { Dispatchers.resetMain() } }
问题2:测试放在test目录而非androidTest目录是否正确?
完全正确!
你写的是纯JVM单元测试,不需要依赖Android系统环境,test目录就是专门用来存放这类测试的。你用到的InstantTaskExecutorRule、Observer等依赖,虽然属于androidx库,但它们都有对应的JVM测试artifact(比如androidx.arch.core:core-testing),可以直接在本地JVM上运行,不需要Android设备或模拟器。
而androidTest目录是用来放仪器化测试的,这类测试需要依赖Android系统(比如测试UI、ContentProvider等),你的ViewModel单元测试完全没必要放在那里。
内容的提问来源于stack exchange,提问作者Praveen P.
相关产品推荐
相关产品推荐

