Kotlin ViewModel单元测试始终通过问题求助
问题分析与解决方案
核心问题根源
- 测试逻辑完全绕过ViewModel:你在测试中直接调用
repository.getAllGames(),这导致测试根本没有验证ViewModel的业务逻辑——ViewModel本应根据网络状态自动触发数据获取,而非手动调用Repository方法。 - 硬编码延迟不可靠:
delay(500L)无法保证协程任务实际执行完成,容易出现测试通过但逻辑未触发的假阳性结果。 - FakeNetworkConnectivityObserver可能存在实现缺陷:如果该类未用
StateFlow发射状态变化,ViewModel的collect逻辑无法接收状态更新,导致数据获取流程不触发。
修复步骤
1. 修正FakeNetworkConnectivityObserver实现
确保它用StateFlow正确发射网络状态变化:
class FakeNetworkConnectivityObserver : ConnectivityObserver { private val _status = MutableStateFlow(Status.Unavailable) override fun observe(): StateFlow<Status> = _status fun setStatus(status: Status) { _status.value = status } }
2. 重构测试代码
移除直接调用Repository的逻辑,改用advanceUntilIdle()等待协程任务完成,确保测试验证ViewModel的实际行为:
@ExperimentalCoroutinesApi class AllGamesViewModelTest{ @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule var mainCoroutineRule = MainCoroutineRule() private lateinit var connectivityObserver: FakeNetworkConnectivityObserver private lateinit var viewModel: AllGamesViewModel private lateinit var getAllGamesUseCase: GetAllGamesUseCase private lateinit var getAllGamesFromLocalUseCase: GetAllGamesFromLocalUseCase private lateinit var getGamesByGenreUseCase: GetGamesByGenreUseCase private lateinit var repository: FakeGameRepository private val games = mutableListOf<Game>() @Before fun setup(){ repository = FakeGameRepository() connectivityObserver = FakeNetworkConnectivityObserver() getAllGamesUseCase = GetAllGamesUseCase(repository) getAllGamesFromLocalUseCase = GetAllGamesFromLocalUseCase(repository) getGamesByGenreUseCase = GetGamesByGenreUseCase(repository) viewModel = AllGamesViewModel( getAllGamesUseCase, getGamesByGenreUseCase, getAllGamesFromLocalUseCase, connectivityObserver ) for(i in 0..10){ val game = Game( developer = "developer $i", freeToGameProfileUrl = "freeUrl $i", gameUrl = "gameUrl $i", genre = "genre $i", id = i, platform = "plat $i", publisher = "publish $i", releaseDate = "release $i", shortDescription = "short $i", thumbnail = "thumb $i", title = "title $i" ) games.add(game) } repository.insertGame(games) } @Test fun `get all games when network is available`() = runTest { connectivityObserver.setStatus(Status.Available) advanceUntilIdle() assertThat(viewModel.status.value).isEqualTo(Status.Available) assertThat(viewModel.state.value.allGames).containsExactlyElementsIn(games) assertThat(viewModel.state.value.isLoading).isFalse() } @Test fun `get all games from local when network is unavailable`() = runTest { connectivityObserver.setStatus(Status.Unavailable) advanceUntilIdle() assertThat(viewModel.status.value).isEqualTo(Status.Unavailable) assertThat(viewModel.state.value.allGames).containsExactlyElementsIn(games) assertThat(viewModel.state.value.isLoading).isFalse() } }
3. 优化ViewModel冗余代码
移除不必要的withContext(Dispatchers.Main)——viewModelScope默认绑定Main调度器,无需手动切换:
fun getAllGames() { viewModelScope.launch { getGame().collect { result -> handleResult(result) } } } fun getAllGamesFromLocal(){ viewModelScope.launch { getAllGamesFromLocalUseCase().collect{ result -> handleResult(result) } } } fun getTaggedGames(genre: String) { viewModelScope.launch { getGamesByGenre(genre).collect { result -> handleResult(result) } } }
内容的提问来源于stack exchange,提问作者Hooman Hooshyar
相关产品推荐
相关产品推荐

