基于Mockito的Firebase Kotlin仓库类单元测试方案咨询
如何为Firebase Firestore的Kotlin仓库类编写单元测试
问题根源
你的测试无效的核心原因是仓库类直接硬编码调用FirebaseFirestore.getInstance(),导致测试中创建的Mock Firestore实例根本没被仓库使用,仓库始终调用真实的Firestore实例,自然会出现Firebase相关错误。另外,Firestore操作是异步的,测试中没有等待回调执行就断言,也会导致结果不符合预期。
第一步:重构仓库类实现依赖注入
先修改RepositoryMockup,通过构造函数注入FirebaseFirestore实例,这样测试时可以传入Mock对象:
package mainPackage.model import com.google.firebase.firestore.FirebaseFirestore import android.util.Log import com.google.firebase.firestore.FirebaseFirestoreException import mainPackage.utils.Checks const val TAG = "FIRESTORE" class RepositoryMockup( // 注入FirebaseFirestore实例,默认用真实实例,测试时传Mock private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance() ) { fun writeNewUser(pass: String, email: String, isATeacher: Boolean?) { // 不再调用getInstance(),用注入的firestore val myRef = firestore.collection("Users").document(email) val newUser = hashMapOf( "is_a_teacher" to isATeacher, "pass" to pass ) myRef.set(newUser) .addOnSuccessListener { Log.d(TAG, "User successfully added") } .addOnFailureListener { e -> Log.w(TAG, "Error writing user", e) } } fun userLoginTest(user: UserTest, callback: (Checks) -> Unit) { val myRef = firestore.collection("Users").document(user.email.toString()) myRef.get() .addOnSuccessListener { document -> if (document != null && document.exists()) { val pass = document.get("pass") as String if (pass == user.pass) { callback(Checks.PASSED) } else { callback(Checks.FAILED_CHECK) } Log.d(TAG, "Pass successfully checked") } else { callback(Checks.NEW_USER_CREATED) Log.d(TAG, "Is empty") writeNewUser(user.pass.toString(), user.email.toString(), user.isATeacher) } } .addOnFailureListener { exception -> if (exception is FirebaseFirestoreException) { Log.e(TAG, "Error getting document: ", exception) } callback(Checks.FAILED_CHECK) } } // 其他方法同理,全部替换为使用注入的firestore实例,不再调用getInstance() // ... 剩余方法省略,按此规则修改 }
第二步:编写正确的单元测试
现在可以用Mockito注入Mock Firestore实例,同时处理异步回调的等待问题(用CountDownLatch):
package mainPackage.tests import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.Task import mainPackage.model.RepositoryMockup import mainPackage.model.UserTest import mainPackage.utils.Checks import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.MockitoAnnotations import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit class RepositoryMockupTest { private lateinit var repositoryMockup: RepositoryMockup @Mock private lateinit var firebaseFirestore: FirebaseFirestore @Mock private lateinit var documentSnapshot: DocumentSnapshot @Mock private lateinit var collectionReference: CollectionReference @Mock private lateinit var documentReference: DocumentReference @Mock private lateinit var task: Task<DocumentSnapshot> @Before fun setUp() { MockitoAnnotations.openMocks(this) // 将Mock的Firestore注入仓库 repositoryMockup = RepositoryMockup(firebaseFirestore) } @Test fun testWriteNewUser() { // 模拟collection和document调用链 `when`(firebaseFirestore.collection("Users")).thenReturn(collectionReference) `when`(collectionReference.document("test@example.com")).thenReturn(documentReference) repositoryMockup.writeNewUser("password123", "test@example.com", true) // 验证是否调用了正确的document和set方法 verify(collectionReference).document("test@example.com") verify(documentReference).set(anyMap<String, Any?>()) } @Test fun testUserLoginTest_Passed() { val user = UserTest("test@example.com", "password123", true) val latch = CountDownLatch(1) var result: Checks? = null // 模拟Firestore调用链 `when`(firebaseFirestore.collection("Users")).thenReturn(collectionReference) `when`(collectionReference.document(user.email)).thenReturn(documentReference) `when`(documentReference.get()).thenReturn(task) // 模拟任务成功完成,返回documentSnapshot `when`(task.addOnSuccessListener(any())).thenAnswer { invocation -> val listener = invocation.getArgument<(DocumentSnapshot) -> Unit>(0) listener.invoke(documentSnapshot) task } `when`(documentSnapshot.exists()).thenReturn(true) `when`(documentSnapshot.get("pass")).thenReturn("password123") // 调用测试方法 repositoryMockup.userLoginTest(user) { result = it latch.countDown() // 回调执行后释放锁 } // 等待回调执行,最多等待2秒 latch.await(2, TimeUnit.SECONDS) // 断言结果 assertEquals(Checks.PASSED, result) verify(documentReference).get() } @Test fun testUserLoginTest_FailedCheck() { val user = UserTest("test@example.com", "wrong_pass", true) val latch = CountDownLatch(1) var result: Checks? = null `when`(firebaseFirestore.collection("Users")).thenReturn(collectionReference) `when`(collectionReference.document(user.email)).thenReturn(documentReference) `when`(documentReference.get()).thenReturn(task) `when`(task.addOnSuccessListener(any())).thenAnswer { invocation -> val listener = invocation.getArgument<(DocumentSnapshot) -> Unit>(0) listener.invoke(documentSnapshot) task } `when`(documentSnapshot.exists()).thenReturn(true) `when`(documentSnapshot.get("pass")).thenReturn("password123") repositoryMockup.userLoginTest(user) { result = it latch.countDown() } latch.await(2, TimeUnit.SECONDS) assertEquals(Checks.FAILED_CHECK, result) } @Test fun testUserLoginTest_NewUserCreated() { val user = UserTest("new@example.com", "password123", true) val latch = CountDownLatch(1) var result: Checks? = null `when`(firebaseFirestore.collection("Users")).thenReturn(collectionReference) `when`(collectionReference.document(user.email)).thenReturn(documentReference) `when`(documentReference.get()).thenReturn(task) `when`(task.addOnSuccessListener(any())).thenAnswer { invocation -> val listener = invocation.getArgument<(DocumentSnapshot) -> Unit>(0) listener.invoke(documentSnapshot) task } `when`(documentSnapshot.exists()).thenReturn(false) // 模拟写入新用户的调用链 `when`(collectionReference.document(user.email)).thenReturn(documentReference) repositoryMockup.userLoginTest(user) { result = it latch.countDown() } latch.await(2, TimeUnit.SECONDS) assertEquals(Checks.NEW_USER_CREATED, result) // 验证是否调用了writeNewUser verify(repositoryMockup).writeNewUser(user.pass, user.email, user.isATeacher) } }
关键注意点
- 依赖注入:必须让仓库类依赖抽象(这里是FirebaseFirestore实例),而不是直接创建实例,否则无法Mock。
- 异步处理:Firestore的所有操作都是异步的,测试时必须用
CountDownLatch、协程或者Mockito的Answer来等待回调执行,否则断言会在回调执行前触发,导致结果错误。 - 调用链模拟:Firestore的API是链式调用(collection -> document -> get/set),所以需要完整模拟这个调用链的每个环节,否则会出现NullPointerException。
内容的提问来源于stack exchange,提问作者real4you
相关产品推荐
相关产品推荐

