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

Kotlin Android测试报错Live Data Value未设置,已尝试多种方法仍无效

问题描述

我是Kotlin Android新手,在学习测试时遇到错误,提示“Live Data Value was never set”,即便使用了advanceUntilIdle也无法解决。实际运行正常,但测试时出现该问题。

相关代码

Repository代码

class UserRepository private constructor(
    private val userPreference: UserPreference,
    private val apiService: ApiService
) {
  
    private val _loginResponse = MutableLiveData<LoginResponse?>()
    val loginResponse: LiveData<LoginResponse?> = _loginResponse

    private val _errorLoginMessage = MutableLiveData<String?>()
    val errorLoginMessage: LiveData<String?> = _errorLoginMessage

    fun login(email: String, password: String) {
        _showLoading.value = true
        val client = apiService.login(email, password)

        client.enqueue(object : Callback<LoginResponse> {
            override fun onResponse(
                call: Call<LoginResponse>, response: Response<LoginResponse>
            ) {
                _showLoading.value = false
                Log.d(TAG, "Register Response : ${response.body()}")
                if (response.isSuccessful && response.body() != null) {
                    _loginResponse.value = response.body()
                } else {
                    _errorLoginMessage.value = response.message()
                    Log.e(TAG, "onFailure: ${response.message()}")
                }
            }

            override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                _showLoading.value = false
                _errorLoginMessage.value = t.message
                Log.e(TAG, "onFailure: ${t.message}")
            }
        })
    }
}

ViewModel代码

class LoginViewModel(private val repository: UserRepository) : ViewModel() {

    val loginResponse: LiveData<LoginResponse?> = repository.loginResponse
    val errorMessage = repository.errorLoginMessage

    fun login(email: String, password: String) {
        repository.login(email, password)
    }
}

测试代码

@RunWith(RobolectricTestRunner::class)
class LoginViewModelTest {

    private lateinit var viewModel: LoginViewModel
    private val context = RuntimeEnvironment.getApplication().applicationContext

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setup() {
        val userRepository = UserRepository.getInstance(
            UserPreference.getInstance(context.dataStore)
        )

        viewModel = LoginViewModel(
            userRepository
        )

        Dispatchers.setMain(StandardTestDispatcher())
    }

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

    @Test
    fun `Test Login Failed`() = runTest {
        viewModel.login("testWrongEmail@gmail.com", "testWrongPassword")
        advanceUntilIdle()
        val result = viewModel.loginResponse.getOrAwaitValue()
        Assert.assertEquals(true, result?.error)
        Assert.assertEquals("failed", result?.message)
    }
}

我已尝试添加InstantTaskExecutorRule、设置主调度器、使用runTest,还将enqueue改为execute,但问题仍未解决,请问这是为什么?


问题根源与解决方案

你的问题出在两个核心点:

  1. 测试用了真实网络请求:OkHttp的异步回调不在协程调度体系内,advanceUntilIdle管不到,测试环境也不会自动触发真实网络请求的回调,导致LiveData没被赋值。
  2. 测试逻辑匹配错误:登录失败场景下,原代码只会更新errorLoginMessage,不会更新loginResponse,你硬要拿loginResponse的值来断言,自然会触发“值未设置”的错误。

具体解决步骤

1. 改造Repository的依赖注入

让UserRepository的getInstance支持传入自定义的ApiService,方便测试时替换成模拟对象:

class UserRepository private constructor(
    private val userPreference: UserPreference,
    private val apiService: ApiService
) {
    companion object {
        @Volatile
        private var INSTANCE: UserRepository? = null

        // 新增apiService参数,默认用真实实例,测试时传模拟实例
        fun getInstance(
            userPreference: UserPreference,
            apiService: ApiService = RetrofitClient.instance.create(ApiService::class.java)
        ): UserRepository {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: UserRepository(userPreference, apiService).also { INSTANCE = it }
            }
        }
    }

    // 原有代码不变...
}

2. 用Mockito模拟ApiService

测试时不要用真实网络,直接模拟ApiService的返回结果:

@RunWith(RobolectricTestRunner::class)
class LoginViewModelTest {

    private lateinit var viewModel: LoginViewModel
    // 模拟ApiService
    private val mockApiService: ApiService = mock(ApiService::class.java)
    private val context = RuntimeEnvironment.getApplication().applicationContext

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setup() {
        val userRepository = UserRepository.getInstance(
            UserPreference.getInstance(context.dataStore),
            mockApiService // 传入模拟的ApiService
        )

        viewModel = LoginViewModel(userRepository)
        Dispatchers.setMain(StandardTestDispatcher())
    }

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

    @Test
    fun `Test Login Failed`() = runTest {
        // 模拟登录失败的响应
        val errorResponseBody = ResponseBody.create(
            MediaType.parse("application/json"),
            "{\"error\":true,\"message\":\"failed\"}"
        )
        val mockErrorResponse = Response.error<LoginResponse>(401, errorResponseBody)
        
        // 指定当调用login方法时返回模拟的失败响应
        `when`(mockApiService.login(anyString(), anyString())).thenReturn(object : Call<LoginResponse> {
            override fun execute(): Response<LoginResponse> = mockErrorResponse
            override fun enqueue(callback: Callback<LoginResponse>) {
                callback.onResponse(this, mockErrorResponse)
            }
            override fun isExecuted(): Boolean = false
            override fun cancel() {}
            override fun isCanceled(): Boolean = false
            override fun request(): Request = Request.Builder().url("http://test.com").build()
            override fun clone(): Call<LoginResponse> = this
        })

        viewModel.login("testWrongEmail@gmail.com", "testWrongPassword")
        advanceUntilIdle()

        // 登录失败时应该断言errorMessage,而非loginResponse
        val errorMsg = viewModel.errorMessage.getOrAwaitValue()
        // 若要匹配"failed",可修改模拟逻辑让response.message()返回该值,或直接模拟回调设置_errorLoginMessage
        Assert.assertEquals("failed", errorMsg)
    }
}

3. 修正测试断言逻辑

登录失败时,原代码会走onResponse的else分支,设置_errorLoginMessage为response.message(),所以测试时应该监听errorMessage,而不是loginResponse——后者在失败场景下根本不会被赋值,这是你之前测试报错的直接原因。

额外提醒

  • 测试异步逻辑时,永远用模拟对象替代真实依赖,真实网络、数据库等操作在测试中不稳定且耗时。
  • advanceUntilIdle只能等待协程调度器中的任务,OkHttp、RxJava等其他异步框架的任务需要单独处理,或者用模拟对象同步返回结果。

内容的提问来源于stack exchange,提问作者Diki Dimal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 02:25:03