Android单元测试中如何触发回调?MVVM场景UI状态验证
问题:MVVM架构下ViewModel回调触发的单元测试验证
我在MVVM架构的应用中,需要测试HomeViewModel里的逻辑:调用addListener()后,当storageRepository.addListener()执行完成时,uiState.isLoading会变为false。
我的HomeViewModel.kt核心代码:
@HiltViewModel class HomeViewModel @Inject constructor( private val storageRepository: StorageRepository, private val accountRepository: AccountRepository, ) : ViewModel() { // 初始isLoading为true var uiState = mutableStateOf(HomeUiState()) ... fun addListener() { viewModelScope.launch { storageRepository.addListener( accountRepository.getUserId(), ::onDocumentEvent, onComplete = { uiState.value = uiState.value.copy(isLoading = false) }, onError = { error -> onAddListenerFailure(error) } ) } } }
测试需求:
- 给定:调用
homeViewModel.addListener() - 当:
storageRepository.addListener(...)执行完成 - 那么:
uiState.isLoading为false
我尝试用Mockito捕获器写了测试,但没达到预期,当前测试代码:
@OptIn(ExperimentalCoroutinesApi::class) internal class HomeViewModelTest { // mock repositories @Mock lateinit var storageRepository: StorageRepository @Mock lateinit var accountRepository: AccountRepository @Mock lateinit var logRepository: LogRepository // 测试调度器 private val dispatcher = StandardTestDispatcher() lateinit var callbackCaptor: KArgumentCaptor<() -> Unit> @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(dispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `loading state is true when viewModel is created`() { val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository) assertTrue(homeViewModel.uiState.value.isLoading) } @Test fun `loading state is false when listener is added successfully`() { val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository) callbackCaptor = argumentCaptor() whenever( storageRepository.addListener( anyString(), anyOrNull(), callbackCaptor.capture(), anyOrNull() ) ) .thenAnswer { callbackCaptor.firstValue.invoke() } homeViewModel.addListener() // 等待状态更新 dispatcher.scheduler.advanceUntilIdle() assertFalse(homeViewModel.uiState.value.isLoading) } }
方案一:修正Mockito捕获器的使用方式
当前测试代码存在两个关键问题导致验证失败:
- 未mock
accountRepository.getUserId()的返回值,导致传入storageRepository.addListener()的userId为null,无法匹配mock的方法调用 - 捕获器的声明和调用逻辑可以优化,提升代码可读性和匹配准确性
修正后的测试代码:
@OptIn(ExperimentalCoroutinesApi::class) internal class HomeViewModelTest { @Mock lateinit var storageRepository: StorageRepository @Mock lateinit var accountRepository: AccountRepository @Mock lateinit var logRepository: LogRepository private val dispatcher = StandardTestDispatcher() @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(dispatcher) // 预先mock getUserId的返回值,确保参数匹配 whenever(accountRepository.getUserId()).thenReturn("test-user-id") } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `loading state turns false after listener is added successfully`() { val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository) // 声明onComplete回调的捕获器 val onCompleteCaptor = argumentCaptor<() -> Unit>() // 精确匹配方法参数,捕获onComplete回调 whenever( storageRepository.addListener( eq("test-user-id"), any(), onCompleteCaptor.capture(), any() ) ).thenAnswer { // 触发捕获到的完成回调 onCompleteCaptor.firstValue.invoke() } homeViewModel.addListener() // 推进协程调度器,直到所有异步任务执行完成 dispatcher.scheduler.advanceUntilIdle() assertFalse(homeViewModel.uiState.value.isLoading) } }
方案二:不使用捕获器,直接触发回调
如果不想依赖Mockito捕获器,可以直接在mock的thenAnswer中获取传入的回调参数并执行,逻辑更直接:
@OptIn(ExperimentalCoroutinesApi::class) internal class HomeViewModelTest { @Mock lateinit var storageRepository: StorageRepository @Mock lateinit var accountRepository: AccountRepository @Mock lateinit var logRepository: LogRepository private val dispatcher = StandardTestDispatcher() @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(dispatcher) whenever(accountRepository.getUserId()).thenReturn("test-user-id") } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `loading state turns false after listener is added successfully`() { val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository) whenever( storageRepository.addListener( eq("test-user-id"), any(), any(), any() ) ).thenAnswer { invocation -> // 直接获取第三个参数(onComplete回调)并执行 val onComplete = invocation.getArgument<() -> Unit>(2) onComplete.invoke() } homeViewModel.addListener() dispatcher.scheduler.advanceUntilIdle() assertFalse(homeViewModel.uiState.value.isLoading) } }
这种方式跳过捕获器,直接通过方法调用的参数索引获取回调,适合参数位置固定的场景,代码更简洁。
内容的提问来源于stack exchange,提问作者Juan Hernandez
相关产品推荐
相关产品推荐

