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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 19:01:01