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,但问题仍未解决,请问这是为什么?
问题根源与解决方案
你的问题出在两个核心点:
- 测试用了真实网络请求:OkHttp的异步回调不在协程调度体系内,
advanceUntilIdle管不到,测试环境也不会自动触发真实网络请求的回调,导致LiveData没被赋值。 - 测试逻辑匹配错误:登录失败场景下,原代码只会更新
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
相关产品推荐
相关产品推荐

