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

Kotlin中Firebase登录函数loginWithEmail带Callback的单元测试问题咨询

登录逻辑单元测试问题解决方案

问题根源

你遇到的两个报错分别由以下原因导致:

  • 首次测试回调不执行:一是你直接使用了真实的FirebaseAuth实例,JVM单元测试环境没有集成Google服务,Firebase请求不会有实际返回;二是你将Thread.sleep写在了主线程Handler的Runnable中,阻塞了主线程,Firebase的异步回调本来需要投递到主线程执行,被阻塞后无法触发,同时assert逻辑在Runnable内部,在回调执行前就已经完成校验。
  • ArgumentCaptor空指针:原生Java版本的ArgumentCaptor.capture()返回值为null,而loginWithEmail方法的loginCallback参数是Kotlin非空类型,Kotlin运行时检测到传入null直接抛出空指针,属于Mockito和Kotlin空安全的兼容问题。

修复前置准备

首先调整LoginManager的构造,将FirebaseAuth通过依赖注入传入,方便测试时注入Mock实例,不需要修改原有业务逻辑:

class LoginManager(private val auth: FirebaseAuth = Firebase.auth) {
    // 原有逻辑不变
    fun loginWithEmail(
            email: String,
            password: String,
            loginCallback: LoginCallback
    ) {
        auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { loginTask ->
            if (loginTask.isSuccessful) {
                auth.currentUser?.let { user ->
                    loginCallback.onLoginSuccess(user)
                    return@addOnCompleteListener
                }
            }
            loginCallback.onLoginFailure(loginTask.exception)
        }
    }
}

测试依赖推荐使用Kotlin适配的Mock框架MockK,也可以选择mockito-kotlin解决空指针兼容问题,测试环境基于Robolectric模拟Android运行环境。

完整测试代码示例

@RunWith(RobolectricTestRunner::class)
class LoginManagerTest {
    // Mock所有外部依赖
    private val mockAuth = mockk<FirebaseAuth>()
    private val mockLoginTask = mockk<Task<AuthResult>>()
    private val mockUser = mockk<FirebaseUser>()

    private lateinit var loginManager: LoginManager

    @Before
    fun setup() {
        loginManager = LoginManager(mockAuth)
        // 模拟登录方法返回自定义的Mock Task
        every { mockAuth.signInWithEmailAndPassword(any(), any()) } returns mockLoginTask
    }

    @Test
    fun `loginWithEmail 登录成功时触发成功回调`() {
        // 构造成功场景
        every { mockLoginTask.isSuccessful } returns true
        every { mockAuth.currentUser } returns mockUser
        // 捕获Firebase Task的完成回调
        val completeListenerSlot = slot<OnCompleteListener<AuthResult>>()
        every { mockLoginTask.addOnCompleteListener(capture(completeListenerSlot)) } answers {
            // 主动触发回调模拟登录完成
            completeListenerSlot.captured.onComplete(mockLoginTask)
            mockLoginTask
        }

        var resultUser: FirebaseUser? = null
        val testCallback = object : LoginCallback {
            override fun onLoginSuccess(user: FirebaseUser) {
                resultUser = user
            }
            override fun onLoginFailure(error: Exception?) = Unit
        }

        loginManager.loginWithEmail("test@example.com", "correct_pwd", testCallback)
        shadowOf(getMainLooper()).idle()

        assert(resultUser == mockUser)
    }

    @Test
    fun `loginWithEmail 登录失败时触发失败回调`() {
        // 构造失败场景
        val testError = Exception("登录校验失败")
        every { mockLoginTask.isSuccessful } returns false
        every { mockLoginTask.exception } returns testError
        val completeListenerSlot = slot<OnCompleteListener<AuthResult>>()
        every { mockLoginTask.addOnCompleteListener(capture(completeListenerSlot)) } answers {
            completeListenerSlot.captured.onComplete(mockLoginTask)
            mockLoginTask
        }

        var resultError: Exception? = null
        val testCallback = object : LoginCallback {
            override fun onLoginSuccess(user: FirebaseUser) = Unit
            override fun onLoginFailure(error: Exception?) {
                resultError = error
            }
        }

        loginManager.loginWithEmail("test@example.com", "wrong_pwd", testCallback)
        shadowOf(getMainLooper()).idle()

        assert(resultError == testError)
    }
}

注意事项

  • 单元测试不要依赖真实的后端服务,所有外部依赖都要做Mock,保证测试结果稳定、执行速度快
  • 不要使用Thread.sleep等待异步任务,优先使用Robolectric的idle机制、CountDownLatch或者测试框架提供的异步等待工具
  • Kotlin项目优先选择适配Kotlin的测试库,避免Java库和Kotlin空安全特性的兼容问题

内容的提问来源于stack exchange,提问作者Dani Garcia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.20 13:27:02