ViewModel协程单元测试无法结束:测试代码挂起无断言结果
TimerViewModel单元测试挂起问题解决
现有以下TimerViewModel实现:
data class TimerInstance( val isEnabled: Boolean, val timeElapsed: Long, ) class TimerViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.Default) : ViewModel() { private val _timerState = MutableStateFlow( TimerInstance(false, 0) ) val timerState = _timerState.asStateFlow() private var timerJob: Job? = null // enable, loop as long as isEnabled, update with incr time fun start() { if (timerJob?.isActive == true) return timerJob = viewModelScope.launch(dispatcher) { _timerState.update { timerState.value.copy(isEnabled = true) } while (_timerState.value.isEnabled) { delay(1000) // in that second pause or stop could have been pressed if (_timerState.value.isEnabled) _timerState.update { _timerState.value.copy( timeElapsed = _timerState.value.timeElapsed + 1 ) } } } } }
对应的单元测试出现挂起问题,无法完成状态更新后的断言:
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.* import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test @ExperimentalCoroutinesApi class TimerViewModelTest { private lateinit var viewModel: TimerViewModel private val testDispatcher = StandardTestDispatcher() @Before fun setup() { Dispatchers.setMain(testDispatcher) viewModel = TimerViewModel(dispatcher = testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun testStart() = runTest { viewModel.start() advanceTimeBy(1000) assertEquals(true, viewModel.timerState.value.isEnabled) assertEquals(1, viewModel.timerState.value.timeElapsed) } }
问题原因
- StandardTestDispatcher的懒执行特性:该调度器不会自动执行挂起任务,调用
advanceTimeBy仅推进时间线,不会触发延迟后的代码逻辑,导致断言时状态未更新。 - StateFlow更新的竞态风险:ViewModel中直接读取
_timerState.value生成新状态,在并发场景下可能获取到过时快照,应该用update方法的lambda参数获取当前最新状态。
解决方案
修正单元测试
在advanceTimeBy后调用runCurrent(),触发调度器执行所有已就绪的任务:
@ExperimentalCoroutinesApi class TimerViewModelTest { private lateinit var viewModel: TimerViewModel private val testDispatcher = StandardTestDispatcher() @Before fun setup() { Dispatchers.setMain(testDispatcher) viewModel = TimerViewModel(dispatcher = testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun testStart() = runTest { viewModel.start() // 先推进时间,再触发任务执行 advanceTimeBy(1000) runCurrent() assertEquals(true, viewModel.timerState.value.isEnabled) assertEquals(1, viewModel.timerState.value.timeElapsed) } }
优化ViewModel实现
使用update方法的lambda参数获取当前状态,避免竞态问题:
class TimerViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.Default) : ViewModel() { private val _timerState = MutableStateFlow( TimerInstance(false, 0) ) val timerState = _timerState.asStateFlow() private var timerJob: Job? = null fun start() { if (timerJob?.isActive == true) return timerJob = viewModelScope.launch(dispatcher) { // 使用lambda参数it获取当前状态 _timerState.update { it.copy(isEnabled = true) } while (_timerState.value.isEnabled) { delay(1000) // 同样用参数获取最新状态 _timerState.update { currentState -> if (currentState.isEnabled) { currentState.copy(timeElapsed = currentState.timeElapsed + 1) } else { currentState } } } } } }
内容的提问来源于stack exchange,提问作者HukeLau_DABA
相关产品推荐
相关产品推荐

