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
相关产品推荐
相关产品推荐

