如何使用 Flow 运行 以 Firestore 作为后端的 Android 应用的 Repository 层的集成测试

How to run an integration test for the Repository layer of an Android app with Firestore as a backend using Flow

我目前正在尝试为我的存储库层编写一个集成测试,以测试我是否调用方法 getExercises(),然后它 returns List<Exercise>,前提是数据是提前加载到本地Firestore模拟器。

到目前为止,我让本地 Firestore 模拟器分别在测试 运行 的 beginning/end 处打开和关闭。我能够将我的数据填充到 Firestore 中,并通过网络在本地 Firestore 模拟器中查看数据 UI.

我的问题是我的测试断言超时,因为 Task(Firestore 库使用的异步构造)阻塞了存储库方法中 await() 部分的线程。

测试

package com.example.fitness.data

import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import com.example.fitness.Constants.EXERCISES_REF
import com.example.fitness.FirebaseEmulatorTest
import com.google.android.gms.tasks.Tasks
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ExerciseRepositoryTest : FirebaseEmulatorTest() {
    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var subject: ExerciseRepository

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @ExperimentalTime
    @Test
    fun `#getExercises returns a flow of exercises`() = runBlocking {
        val exercises = mutableListOf<Exercise>().apply {
            add(Exercise("a", "pushups"))
            add(Exercise("b", "pull-ups"))
            add(Exercise("c", "sit-ups"))
        }

        runBlocking(Dispatchers.IO) {
            val task1 = firestoreInstance.collection(EXERCISES_REF).add(exercises.first())
            val task2 = firestoreInstance.collection(EXERCISES_REF).add(exercises[1])
            val task3 = firestoreInstance.collection(EXERCISES_REF).add(exercises.last())

            Tasks.await(task1)
            Tasks.await(task2)
            Tasks.await(task3)

            println("Done with tasks: task1: ${task1.isComplete}. task2: ${task2.isComplete}. task3: ${task3.isComplete}.")
        }

        println("About to get exercises")

        subject.getExercises().test(timeout = Duration.seconds(5)) {
            println("test body")

            assertThat(awaitItem().size, `is`(4)) // Just checking that it passes for the right reasons first. This number should be 3
        }
    }
}

存储库(被测系统)

package com.example.fitness.data

import com.example.fitness.Constants.EXERCISES_REF
import com.google.firebase.firestore.CollectionReference
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

@Singleton
class ExerciseRepository @Inject constructor(
    @Named(EXERCISES_REF) private val exerciseCollRef: CollectionReference
) {
    fun getExercises() = flow<List<Exercise>> {
        println("beginning of searchForExercise")

        val exercises = exerciseCollRef.limit(5).get().await() // NEVER FINISHES!!
        println("Exercise count: ${exercises.documents}")

        emit(exercises.toObjects(Exercise::class.java))
    }
}

此结果的输出结果为:

Done with tasks: task1: true. task2: true. task3: true.
About to search for exercises
beginning of searchForExercise
test body

Timed out waiting for 5000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 5000 ms

“锻炼次数:3”消息从不打印!

注意:我正在使用 Robolectric 4.6.1,kotlinx-coroutines-playservices (1.5.0) 提供 await() 扩展功能,以及用于流断言的 Turbine 测试库 (0.6 .1)

可能相关的是此测试继承的超类,它将主调度程序设置为测试调度程序。

package com.example.fitness

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Rule

abstract class CoroutineTest {
    @Rule
    @JvmField
    val rule = InstantTaskExecutorRule()

    protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
    private val testCoroutineScope = TestCoroutineScope(testDispatcher)

    @Before
    fun setupViewModelScope() {
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun cleanupViewModelScope() {
        Dispatchers.resetMain()
    }

    @After
    fun cleanupCoroutines() {
        testDispatcher.cleanupTestCoroutines()
        testDispatcher.resumeDispatcher()
    }

    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
        testCoroutineScope.runBlockingTest(block)
}

如有任何帮助,我们将不胜感激。

编辑 我已经打开了一个 issue with the kotlin extensions team to get more visibility on how to go about testing this, including a repo 来演示这个问题。

此问题已在 kotlinx-coroutines 软件包 (1.6.0-RC) 的新版本中得到解决。 See my github compare 跨分支。此版本的测试现在按预期通过。