如何使用 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 跨分支。此版本的测试现在按预期通过。
我目前正在尝试为我的存储库层编写一个集成测试,以测试我是否调用方法 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 跨分支。此版本的测试现在按预期通过。