You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
    }
}

问题原因

  1. StandardTestDispatcher的懒执行特性:该调度器不会自动执行挂起任务,调用advanceTimeBy仅推进时间线,不会触发延迟后的代码逻辑,导致断言时状态未更新。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 08:05:17