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

ViewModel协程测试中LiveData预期MyState.Success却返回null

问题描述

在ViewModel单元测试中,调用interactor("AnyInteractor")后,断言LiveData的state值应为MyState.Success,但实际返回null,测试失败:

预期值:com.br.testesunitarios.ui.MyState$Success@62fad19
实际值:null

疑问

  • 是否是LiveData异步发射值导致了这种不匹配?
  • 是否需要采用不同的测试策略来处理协程中的LiveData更新?
  • 如何确保测试等待到最终的MyState.Success状态被发射?

ViewModel实现

package com.br.testesunitarios.ui.home

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.br.testesunitarios.ui.MyState
import com.br.testesunitarios.ui.ProviderUseCase
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class HomeViewModel : ViewModel() {

    private val _state: MutableLiveData<MyState> = MutableLiveData()
    val state: LiveData<MyState> = _state

    fun interactor(interactor: String) {
        when (interactor) {
            "AnyInteractor" -> {
                executarMetodo()
            }
        }
    }

    private fun executarMetodo() {
        viewModelScope.launch {
            try {
                _state.value = MyState.Loading
                delay(1000)
                _state.value = MyState.Success
                delay(1000)
                when (ProviderUseCase().execute()) {
                    MyState.Success -> {
                        _state.value = MyState.Success
                    }
                    else -> {
                        _state.value = MyState.Failure
                    }
                }
            } catch (e: Exception) {
                _state.value = MyState.Failure
            }
        }
    }
}

测试用例

package com.br.testesunitarios.ui.home

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.br.testesunitarios.ui.MyState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.mockito.junit.MockitoJUnitRunner
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class HomeViewModelTest {

    val instantExecutorRule = InstantTaskExecutorRule()
    private val testDispatcher = StandardTestDispatcher()

    @BeforeEach
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest {
        val viewModel = HomeViewModel()

        viewModel.interactor("AnyInteractor")

        assertEquals(MyState.Success, viewModel.state.value)  // 此行测试失败
    }
}

问题分析与解决方案

1. 核心问题拆解

测试失败的关键原因有两个:

  • 协程任务未被触发:使用StandardTestDispatcher时,delay等挂起函数不会自动执行,测试代码直接断言时,协程逻辑还没跑完
  • LiveData更新未被同步捕获:LiveData的value获取是即时的,而ViewModel中的状态更新是异步的,直接读取会拿到初始的null值
  • 规则未生效:InstantTaskExecutorRule未加@get:Rule注解,导致LiveData无法在测试中同步执行

2. 针对性解决方案

方案一:修复测试逻辑,等待协程与LiveData更新

修改测试用例,解决三个问题:启用InstantTaskExecutorRule、推进协程调度器、捕获LiveData的最终状态:

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class HomeViewModelTest {

    @get:Rule // 必须加此注解才能启用规则
    val instantExecutorRule = InstantTaskExecutorRule()
    private val testDispatcher = StandardTestDispatcher()

    @BeforeEach
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
        // Mock ProviderUseCase,确保返回预期的Success
        mockStatic(ProviderUseCase::class.java).use { mocked ->
            mocked.`when`<MyState> { ProviderUseCase().execute() }
                .thenReturn(MyState.Success)
        }
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest {
        val viewModel = HomeViewModel()
        var capturedState: MyState? = null

        // 观察LiveData,捕获所有状态更新
        viewModel.state.observeForever { state ->
            capturedState = state
        }

        viewModel.interactor("AnyInteractor")
        // 推进调度器,让所有协程任务(包括两次delay)执行完毕
        testDispatcher.scheduler.advanceUntilIdle()

        assertEquals(MyState.Success, capturedState)
    }
}

方案二:用工具类简化LiveData测试

可以封装一个工具类,统一处理LiveData的等待逻辑:

import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

object LiveDataTestUtil {
    fun <T> LiveData<T>.getOrAwaitValue(timeout: Long = 2): T {
        var data: T? = null
        val latch = CountDownLatch(1)

        val observer = Observer<T> { value ->
            data = value
            latch.countDown()
        }

        observeForever(observer)

        // 等待状态更新,超时则抛出异常
        latch.await(timeout, TimeUnit.SECONDS)
        removeObserver(observer)

        @Suppress("UNCHECKED_CAST")
        return data as T
    }
}

使用工具类后的测试代码更简洁:

@Test
fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest {
    val viewModel = HomeViewModel()

    viewModel.interactor("AnyInteractor")
    testDispatcher.scheduler.advanceUntilIdle()

    assertEquals(MyState.Success, viewModel.state.getOrAwaitValue())
}

3. ViewModel优化建议

为了提升可测试性,建议调整ViewModel的依赖注入方式:

class HomeViewModel(
    // 通过构造函数注入依赖,测试时可传入Mock实例
    private val providerUseCase: ProviderUseCase = ProviderUseCase()
) : ViewModel() {

    private val _state: MutableLiveData<MyState> = MutableLiveData()
    val state: LiveData<MyState> = _state

    fun interactor(interactor: String) {
        when (interactor) {
            "AnyInteractor" -> executarMetodo()
        }
    }

    private fun executarMetodo() {
        viewModelScope.launch {
            try {
                _state.value = MyState.Loading
                delay(1000)
                // 合并重复的状态设置逻辑
                val useCaseResult = providerUseCase.execute()
                _state.value = if (useCaseResult is MyState.Success) MyState.Success else MyState.Failure
            } catch (e: Exception) {
                _state.value = MyState.Failure
            }
        }
    }
}

此时测试可直接传入Mock的UseCase,无需静态Mock:

@Test
fun `should execute executarMetodo when interactor is AnyInteractor`() = runTest {
    val mockUseCase = mock(ProviderUseCase::class.java)
    `when`(mockUseCase.execute()).thenReturn(MyState.Success)

    val viewModel = HomeViewModel(mockUseCase)
    var capturedState: MyState? = null

    viewModel.state.observeForever { capturedState = it }

    viewModel.interactor("AnyInteractor")
    testDispatcher.scheduler.advanceUntilIdle()

    assertEquals(MyState.Success, capturedState)
}

内容的提问来源于stack exchange,提问作者Kleyton Prestes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:04:53