运行 在 Kotlin 流程中测试 Retrofit 协程时出现 TimeoutCancellationException
TimeoutCancellationException when running tests for a Retrofit coroutine in a Kotlin flow
我有一个创建流的存储库,我在其中发出暂停 Retrofit 方法的结果。这在应用程序中有效,但我想 运行 测试代码。
我在测试中使用 kotlinx-coroutines-test v1.6.0 和 MockWebServer v4.9.3。当我尝试 运行 测试时,我得到:
Timed out waiting for 1000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:23)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
(Coroutine boundary)
at app.cash.turbine.ChannelBasedFlowTurbine$awaitEvent.invokeSuspend(FlowTurbine.kt:247)
at app.cash.turbine.ChannelBasedFlowTurbine$withTimeout.invokeSuspend(FlowTurbine.kt:215)
at app.cash.turbine.ChannelBasedFlowTurbine.awaitItem(FlowTurbine.kt:252)
at ogbe.eva.prompt.home.HomeRepositoryTest$currentTask when server responds with error emits failure.invokeSuspend(HomeRepositoryTest.kt:90)
at app.cash.turbine.FlowTurbineKt$test.invokeSuspend(FlowTurbine.kt:86)
at ogbe.eva.prompt.home.HomeRepositoryTest$currentTask when server responds with error emits failure.invokeSuspend(HomeRepositoryTest.kt:89)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine.invokeSuspend(TestBuilders.kt:208)
(Coroutine creation stacktrace)
at app//kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:184)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:161)
at app//kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
at app//ogbe.eva.prompt.TestCoroutineRule.runTest(TestCoroutineRule.kt:26)
at app//ogbe.eva.prompt.home.HomeRepositoryTest.currentTask when server responds with error emits failure(HomeRepositoryTest.kt:84)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base@11.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@11.0.11/java.lang.reflect.Method.invoke(Method.java:566)
at app//org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:59)
at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at app//org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at app//org.junit.rules.TestWatcher.evaluate(TestWatcher.java:61)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:306)
at app//org.junit.runners.BlockJUnit4ClassRunner.evaluate(BlockJUnit4ClassRunner.java:100)
at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at app//org.junit.runners.ParentRunner.run(ParentRunner.java:331)
at app//org.junit.runners.ParentRunner.schedule(ParentRunner.java:79)
at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at app//org.junit.runners.ParentRunner.access0(ParentRunner.java:66)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:293)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:306)
at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base@11.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@11.0.11/java.lang.reflect.Method.invoke(Method.java:566)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.run(TestWorker.java:176)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:23)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at app//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at app//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at app//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
at app//kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
... 50 more
我没有机会 运行 我的测试断言,而这正是我想要做的。它只是因这个意外错误而失败。
我已经检查了在流中调用随机挂起函数并 运行在我的流函数之外设置模拟服务器。这两个都将在没有超时错误的情况下完成,但是当我将流程与测试和改造结合起来时,它显示超时错误。
存储库代码:
class HomeRepository @Inject constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val promptService: PromptService,
) {
val currentTask = flow {
try {
val response = promptService.getSchedule(1) // Suspending Retrofit method that fails the tests
if (response.isSuccessful) {
val schedule = response.body()
if (schedule == null) {
Log.e(TAG, "Get schedule response has empty body")
emit(LoadState.Failure())
} else {
emit(LoadState.Data(schedule.tasks.first()))
}
} else {
Log.e(
TAG,
"Server responded to get schedule request with error: ${response.message()}"
)
emit(LoadState.Failure())
}
} catch (e: Exception) {
Log.e(TAG, "Could not get schedule from server", e)
emit(LoadState.Failure())
}
}
.flowOn(ioDispatcher)
companion object {
private val TAG = HomeRepository::class.simpleName
}
}
测试代码:
@OptIn(ExperimentalCoroutinesApi::class)
class HomeRepositoryTest {
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val mockWebServer = MockWebServer()
@Before
fun setUp() {
mockWebServer.start()
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `currentTask when server responds with error emits failure`() = testCoroutineRule.runTest {
mockWebServer.enqueue(MockResponse().setResponseCode(500))
val homeRepository = createRepository()
homeRepository.currentTask.test {
expectThat(awaitItem()).isFailure()
awaitComplete()
}
}
private fun createRepository(promptService: PromptService = createPromptService()) =
HomeRepository(testCoroutineRule.testDispatcher, promptService)
private fun createPromptService(): PromptService {
val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.SECONDS)
.writeTimeout(1, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(PromptService::class.java)
}
}
规则代码:
@OptIn(ExperimentalCoroutinesApi::class)
class TestCoroutineRule : TestWatcher() {
val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
fun runTest(block: suspend TestScope.() -> Unit) =
testScope.runTest(testBody = block)
}
如何在不出现超时错误的情况下测试使用 Retrofit 的流程?
我还没有想出如何直接测试 currentTask
流 属性。但我确实找到了一个可以让我测试大部分功能的解决方法。
我在不引用 Retrofit 的情况下拆分了一个处理大部分逻辑的基本存储库。我还从 属性 流程中分离出一个挂起方法,这样我就可以在不通过流程的情况下测试 Retrofit 逻辑。
基础存储库:
abstract class BaseLoadingRepository(private val ioDispatcher: CoroutineDispatcher) {
protected suspend fun <NetworkResponse : Any, DbResponse : Any> loadFromServer(
callNetwork: suspend () -> Response<NetworkResponse>,
mapResponse: (NetworkResponse) -> DbResponse?,
saveData: suspend (DbResponse?) -> Unit,
staleData: DbResponse? = null,
) = withContext(ioDispatcher) {
try {
val response = callNetwork()
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
Log.e(TAG, "Server response has empty body")
LoadState.Failure(staleData)
} else {
val dbData = mapResponse(body)
saveData(dbData)
LoadState.toContent(dbData)
}
} else {
Log.e(
TAG,
"Server responded with error: ${response.message()}"
)
LoadState.Failure(staleData)
}
} catch (e: Exception) {
Log.e(TAG, "Could not load data from server", e)
LoadState.Failure(staleData)
}
}
protected fun <T : Any> getLoadedFlow(
getFromDb: () -> Flow<T?>,
getFromNetwork: suspend (T?) -> Unit,
canEmitCache: (T) -> Boolean = { true },
) = flow {
try {
val cachedData = getFromDb().firstOrNull()
cachedData?.let {
if (canEmitCache(cachedData)) {
emit(LoadState.Data(cachedData))
}
}
getFromNetwork(cachedData)
val refreshedData = getFromDb().map(LoadState.Companion::toContent)
emitAll(refreshedData)
} catch (e: Exception) {
Log.e(TAG, "Could not load data", e)
emit(LoadState.Failure())
}
}.flowOn(ioDispatcher)
companion object {
private val TAG = BaseLoadingRepository::class.simpleName
}
}
具体存储库:
class HomeRepository @Inject constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val promptDatabase: PromptDatabase,
private val promptService: PromptService,
) : BaseLoadingRepository(ioDispatcher) {
val currentTask =
getLoadedFlow<Task>({ promptDatabase.taskDao().getCurrentTask() }, this::loadCurrentTask)
@VisibleForTesting
internal suspend fun loadCurrentTask(cachedData: Task? = null) =
loadFromServer(
{ promptService.getSchedule(1) },
{
it.tasks.firstOrNull()?.toTask(0)
},
{ task ->
promptDatabase.withTransaction {
promptDatabase.taskDao().clearPositions()
task?.let { promptDatabase.taskDao().insert(it) }
}
},
cachedData
)
}
基础存储库测试:
@OptIn(ExperimentalCoroutinesApi::class)
class BaseLoadingRepositoryTest {
@MockK
lateinit var response: Response<String>
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
MockKAnnotations.init(this)
}
@Test
fun `loadFromServer with network response body returns mapped data`() = testScope.runTest {
every { response.isSuccessful } returns true
every { response.body() } returns "2"
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {})
expectThat(result).data.isEqualTo(2)
}
@Test
fun `loadFromServer with network response body saves data in database`() = testScope.runTest {
val saveData = mockk<(Int?) -> Unit>(relaxed = true)
every { response.isSuccessful } returns true
every { response.body() } returns "2"
val testRepository = createRepository()
testRepository.publicLoadFromServer({ response }, String::toInt, saveData)
verify { saveData(2) }
}
@Test
fun `loadFromServer without network response body returns failure`() = testScope.runTest {
val staleData = 2
every { response.isSuccessful } returns true
every { response.body() } returns null
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {}, staleData)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `loadFromServer when network responds with error returns failure`() = testScope.runTest {
val staleData = 2
every { response.isSuccessful } returns false
every { response.message() } returns "Internal Server Error"
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {}, staleData)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `loadFromServer when throws returns failure`() = testScope.runTest {
val staleData = 2
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer(
{ throw RuntimeException("Oh no!") },
String::toInt,
{},
staleData
)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `getLoadedFlow emits refreshed data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns emptyFlow() andThen flowOf(2)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { false })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
awaitComplete()
}
}
@Test
fun `getLoadedFlow loads from the network`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
val getFromNetwork = mockk<(Int?) -> Unit>(relaxed = true)
every { getFromDb() } returns flowOf(2) andThen flowOf(3)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, getFromNetwork) { false }
result.test {
expectThat(awaitItem()).data.isEqualTo(3)
awaitComplete()
verify { getFromNetwork(2) }
}
}
@Test
fun `getLoadedFlow when can emit cached data emits cached data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns flowOf(2) andThen flowOf(3)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { true })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
expectThat(awaitItem()).data.isEqualTo(3)
awaitComplete()
}
}
@Test
fun `getLoadedFlow when cached data null does not emit cached data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns flowOf(null) andThen flowOf(2)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { true })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
awaitComplete()
}
}
@Test
fun `getLoadedFlow when throws emits failure`() = testScope.runTest {
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow<Any>(
{ throw RuntimeException("Oh no!") },
{},
{ false }
)
result.test {
expectThat(awaitItem()).isFailure()
awaitComplete()
}
}
private fun createRepository() = TestRepository(testDispatcher)
private class TestRepository(testDispatcher: CoroutineDispatcher) :
BaseLoadingRepository(testDispatcher) {
suspend fun <NetworkResponse : Any, DbResponse : Any> publicLoadFromServer(
callNetwork: suspend () -> Response<NetworkResponse>,
mapResponse: (NetworkResponse) -> DbResponse?,
saveData: suspend (DbResponse?) -> Unit,
staleData: DbResponse? = null
) = loadFromServer(callNetwork, mapResponse, saveData, staleData)
fun <T : Any> publicGetLoadedFlow(
getFromDb: () -> Flow<T?>,
getFromNetwork: suspend (T?) -> Unit,
canEmitCache: (T) -> Boolean,
) = getLoadedFlow(getFromDb, getFromNetwork, canEmitCache)
}
}
具体存储库测试:
@OptIn(ExperimentalCoroutinesApi::class)
class HomeRepositoryTest {
private val promptDatabase = createPromptDatabase()
private val mockWebServer = MockWebServer()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
mockWebServer.start()
mockWebServer.enqueueFile("get_current_task_response.json")
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun loadCurrentTask_returnsCurrentTask() = testScope.runTest {
val homeRepository = createRepository()
val result = homeRepository.loadCurrentTask()
expectThat(result).data.isEqualTo(TestData.CurrentTask)
}
@Test
fun loadCurrentTask_loadsCurrentTaskNetwork() = testScope.runTest {
val homeRepository = createRepository()
homeRepository.loadCurrentTask()
runCatching {
val request = mockWebServer.takeRequest(1, TimeUnit.SECONDS)
expectThat(request).hasRequestLine("GET /schedule?size=1")
}
}
@Test
fun loadCurrentTask_insertsCurrentTaskInDatabase() = testScope.runTest {
val homeRepository = createRepository()
homeRepository.loadCurrentTask()
val schedule = promptDatabase.taskDao().getSchedule().first()
expectThat(schedule).containsExactly(TestData.CurrentTask)
}
private fun createRepository(): HomeRepository {
val promptService = createPromptService(mockWebServer.url("/"))
return HomeRepository(testDispatcher, promptDatabase, promptService)
}
}
这让我完成了 90% 的事情。直接测试 currentTask
属性 还是不错的,因为那里还剩下一点逻辑。
我运行遇到了同样的问题。我发现这一切都是因为 kotlin-coroutines-test 1.6.0,更具体地说是 运行 测试行为。
运行Test 允许您控制虚拟时间(如 运行BlockingTest),这在测试时很方便。但是你这里的问题是你正在使用 OkHttp 的 Retrofit(测试中的 MockWebServer),它 运行 在它自己的线程内并且在测试调度程序之外使用实时。
这里的解决方案是
- 使用运行阻塞而不是运行测试是否不需要控制虚拟时间,例如调用 delay()
- 运行 通过使用“withContext(Dispatchers.Default)”或“withContext(Dispatchers.IO)”
包装您的测试内容,您在另一个调度程序上的测试
@Test
fun `my unit test`() = runTest {
withContext(Dispatchers.Default) { // can be either Dispatchers.Default or Dispatchers.IO but not Dispatchers.Main
// enter code here
}
}
另一种解决方案是通过在测试 class 中创建 属性 来为主调度程序提供不同的线程上下文,例如:
@OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
@Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
}
@After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
@Test
fun `my unit test`() = runTest {
withContext(Dispatchers.Main) { // this is not mandatory here as the default dispatcher is Main
// enter code here
}
}
我对协同程序和协同程序测试还很陌生,所以我可能误解了一些细节,但我希望它能有所帮助。
文档也可以派上用场:https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md
我有一个创建流的存储库,我在其中发出暂停 Retrofit 方法的结果。这在应用程序中有效,但我想 运行 测试代码。
我在测试中使用 kotlinx-coroutines-test v1.6.0 和 MockWebServer v4.9.3。当我尝试 运行 测试时,我得到:
Timed out waiting for 1000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:23)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
(Coroutine boundary)
at app.cash.turbine.ChannelBasedFlowTurbine$awaitEvent.invokeSuspend(FlowTurbine.kt:247)
at app.cash.turbine.ChannelBasedFlowTurbine$withTimeout.invokeSuspend(FlowTurbine.kt:215)
at app.cash.turbine.ChannelBasedFlowTurbine.awaitItem(FlowTurbine.kt:252)
at ogbe.eva.prompt.home.HomeRepositoryTest$currentTask when server responds with error emits failure.invokeSuspend(HomeRepositoryTest.kt:90)
at app.cash.turbine.FlowTurbineKt$test.invokeSuspend(FlowTurbine.kt:86)
at ogbe.eva.prompt.home.HomeRepositoryTest$currentTask when server responds with error emits failure.invokeSuspend(HomeRepositoryTest.kt:89)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine.invokeSuspend(TestBuilders.kt:208)
(Coroutine creation stacktrace)
at app//kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:184)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:161)
at app//kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
at app//ogbe.eva.prompt.TestCoroutineRule.runTest(TestCoroutineRule.kt:26)
at app//ogbe.eva.prompt.home.HomeRepositoryTest.currentTask when server responds with error emits failure(HomeRepositoryTest.kt:84)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base@11.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@11.0.11/java.lang.reflect.Method.invoke(Method.java:566)
at app//org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:59)
at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at app//org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at app//org.junit.rules.TestWatcher.evaluate(TestWatcher.java:61)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:306)
at app//org.junit.runners.BlockJUnit4ClassRunner.evaluate(BlockJUnit4ClassRunner.java:100)
at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at app//org.junit.runners.ParentRunner.run(ParentRunner.java:331)
at app//org.junit.runners.ParentRunner.schedule(ParentRunner.java:79)
at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at app//org.junit.runners.ParentRunner.access0(ParentRunner.java:66)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:293)
at app//org.junit.runners.ParentRunner.evaluate(ParentRunner.java:306)
at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@11.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base@11.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@11.0.11/java.lang.reflect.Method.invoke(Method.java:566)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.run(TestWorker.java:176)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:23)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
at app//kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at app//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at app//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at app//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at app//kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
at app//kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
... 50 more
我没有机会 运行 我的测试断言,而这正是我想要做的。它只是因这个意外错误而失败。
我已经检查了在流中调用随机挂起函数并 运行在我的流函数之外设置模拟服务器。这两个都将在没有超时错误的情况下完成,但是当我将流程与测试和改造结合起来时,它显示超时错误。
存储库代码:
class HomeRepository @Inject constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val promptService: PromptService,
) {
val currentTask = flow {
try {
val response = promptService.getSchedule(1) // Suspending Retrofit method that fails the tests
if (response.isSuccessful) {
val schedule = response.body()
if (schedule == null) {
Log.e(TAG, "Get schedule response has empty body")
emit(LoadState.Failure())
} else {
emit(LoadState.Data(schedule.tasks.first()))
}
} else {
Log.e(
TAG,
"Server responded to get schedule request with error: ${response.message()}"
)
emit(LoadState.Failure())
}
} catch (e: Exception) {
Log.e(TAG, "Could not get schedule from server", e)
emit(LoadState.Failure())
}
}
.flowOn(ioDispatcher)
companion object {
private val TAG = HomeRepository::class.simpleName
}
}
测试代码:
@OptIn(ExperimentalCoroutinesApi::class)
class HomeRepositoryTest {
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val mockWebServer = MockWebServer()
@Before
fun setUp() {
mockWebServer.start()
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `currentTask when server responds with error emits failure`() = testCoroutineRule.runTest {
mockWebServer.enqueue(MockResponse().setResponseCode(500))
val homeRepository = createRepository()
homeRepository.currentTask.test {
expectThat(awaitItem()).isFailure()
awaitComplete()
}
}
private fun createRepository(promptService: PromptService = createPromptService()) =
HomeRepository(testCoroutineRule.testDispatcher, promptService)
private fun createPromptService(): PromptService {
val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.SECONDS)
.writeTimeout(1, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(PromptService::class.java)
}
}
规则代码:
@OptIn(ExperimentalCoroutinesApi::class)
class TestCoroutineRule : TestWatcher() {
val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
fun runTest(block: suspend TestScope.() -> Unit) =
testScope.runTest(testBody = block)
}
如何在不出现超时错误的情况下测试使用 Retrofit 的流程?
我还没有想出如何直接测试 currentTask
流 属性。但我确实找到了一个可以让我测试大部分功能的解决方法。
我在不引用 Retrofit 的情况下拆分了一个处理大部分逻辑的基本存储库。我还从 属性 流程中分离出一个挂起方法,这样我就可以在不通过流程的情况下测试 Retrofit 逻辑。
基础存储库:
abstract class BaseLoadingRepository(private val ioDispatcher: CoroutineDispatcher) {
protected suspend fun <NetworkResponse : Any, DbResponse : Any> loadFromServer(
callNetwork: suspend () -> Response<NetworkResponse>,
mapResponse: (NetworkResponse) -> DbResponse?,
saveData: suspend (DbResponse?) -> Unit,
staleData: DbResponse? = null,
) = withContext(ioDispatcher) {
try {
val response = callNetwork()
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
Log.e(TAG, "Server response has empty body")
LoadState.Failure(staleData)
} else {
val dbData = mapResponse(body)
saveData(dbData)
LoadState.toContent(dbData)
}
} else {
Log.e(
TAG,
"Server responded with error: ${response.message()}"
)
LoadState.Failure(staleData)
}
} catch (e: Exception) {
Log.e(TAG, "Could not load data from server", e)
LoadState.Failure(staleData)
}
}
protected fun <T : Any> getLoadedFlow(
getFromDb: () -> Flow<T?>,
getFromNetwork: suspend (T?) -> Unit,
canEmitCache: (T) -> Boolean = { true },
) = flow {
try {
val cachedData = getFromDb().firstOrNull()
cachedData?.let {
if (canEmitCache(cachedData)) {
emit(LoadState.Data(cachedData))
}
}
getFromNetwork(cachedData)
val refreshedData = getFromDb().map(LoadState.Companion::toContent)
emitAll(refreshedData)
} catch (e: Exception) {
Log.e(TAG, "Could not load data", e)
emit(LoadState.Failure())
}
}.flowOn(ioDispatcher)
companion object {
private val TAG = BaseLoadingRepository::class.simpleName
}
}
具体存储库:
class HomeRepository @Inject constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val promptDatabase: PromptDatabase,
private val promptService: PromptService,
) : BaseLoadingRepository(ioDispatcher) {
val currentTask =
getLoadedFlow<Task>({ promptDatabase.taskDao().getCurrentTask() }, this::loadCurrentTask)
@VisibleForTesting
internal suspend fun loadCurrentTask(cachedData: Task? = null) =
loadFromServer(
{ promptService.getSchedule(1) },
{
it.tasks.firstOrNull()?.toTask(0)
},
{ task ->
promptDatabase.withTransaction {
promptDatabase.taskDao().clearPositions()
task?.let { promptDatabase.taskDao().insert(it) }
}
},
cachedData
)
}
基础存储库测试:
@OptIn(ExperimentalCoroutinesApi::class)
class BaseLoadingRepositoryTest {
@MockK
lateinit var response: Response<String>
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
MockKAnnotations.init(this)
}
@Test
fun `loadFromServer with network response body returns mapped data`() = testScope.runTest {
every { response.isSuccessful } returns true
every { response.body() } returns "2"
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {})
expectThat(result).data.isEqualTo(2)
}
@Test
fun `loadFromServer with network response body saves data in database`() = testScope.runTest {
val saveData = mockk<(Int?) -> Unit>(relaxed = true)
every { response.isSuccessful } returns true
every { response.body() } returns "2"
val testRepository = createRepository()
testRepository.publicLoadFromServer({ response }, String::toInt, saveData)
verify { saveData(2) }
}
@Test
fun `loadFromServer without network response body returns failure`() = testScope.runTest {
val staleData = 2
every { response.isSuccessful } returns true
every { response.body() } returns null
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {}, staleData)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `loadFromServer when network responds with error returns failure`() = testScope.runTest {
val staleData = 2
every { response.isSuccessful } returns false
every { response.message() } returns "Internal Server Error"
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer({ response }, String::toInt, {}, staleData)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `loadFromServer when throws returns failure`() = testScope.runTest {
val staleData = 2
val testRepository = createRepository()
val result = testRepository.publicLoadFromServer(
{ throw RuntimeException("Oh no!") },
String::toInt,
{},
staleData
)
expectThat(result).failureData.isEqualTo(staleData)
}
@Test
fun `getLoadedFlow emits refreshed data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns emptyFlow() andThen flowOf(2)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { false })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
awaitComplete()
}
}
@Test
fun `getLoadedFlow loads from the network`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
val getFromNetwork = mockk<(Int?) -> Unit>(relaxed = true)
every { getFromDb() } returns flowOf(2) andThen flowOf(3)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, getFromNetwork) { false }
result.test {
expectThat(awaitItem()).data.isEqualTo(3)
awaitComplete()
verify { getFromNetwork(2) }
}
}
@Test
fun `getLoadedFlow when can emit cached data emits cached data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns flowOf(2) andThen flowOf(3)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { true })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
expectThat(awaitItem()).data.isEqualTo(3)
awaitComplete()
}
}
@Test
fun `getLoadedFlow when cached data null does not emit cached data`() = testScope.runTest {
val getFromDb = mockk<() -> Flow<Int?>>(relaxed = true)
every { getFromDb() } returns flowOf(null) andThen flowOf(2)
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow(getFromDb, {}, { true })
result.test {
expectThat(awaitItem()).data.isEqualTo(2)
awaitComplete()
}
}
@Test
fun `getLoadedFlow when throws emits failure`() = testScope.runTest {
val testRepository = createRepository()
val result = testRepository.publicGetLoadedFlow<Any>(
{ throw RuntimeException("Oh no!") },
{},
{ false }
)
result.test {
expectThat(awaitItem()).isFailure()
awaitComplete()
}
}
private fun createRepository() = TestRepository(testDispatcher)
private class TestRepository(testDispatcher: CoroutineDispatcher) :
BaseLoadingRepository(testDispatcher) {
suspend fun <NetworkResponse : Any, DbResponse : Any> publicLoadFromServer(
callNetwork: suspend () -> Response<NetworkResponse>,
mapResponse: (NetworkResponse) -> DbResponse?,
saveData: suspend (DbResponse?) -> Unit,
staleData: DbResponse? = null
) = loadFromServer(callNetwork, mapResponse, saveData, staleData)
fun <T : Any> publicGetLoadedFlow(
getFromDb: () -> Flow<T?>,
getFromNetwork: suspend (T?) -> Unit,
canEmitCache: (T) -> Boolean,
) = getLoadedFlow(getFromDb, getFromNetwork, canEmitCache)
}
}
具体存储库测试:
@OptIn(ExperimentalCoroutinesApi::class)
class HomeRepositoryTest {
private val promptDatabase = createPromptDatabase()
private val mockWebServer = MockWebServer()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
mockWebServer.start()
mockWebServer.enqueueFile("get_current_task_response.json")
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun loadCurrentTask_returnsCurrentTask() = testScope.runTest {
val homeRepository = createRepository()
val result = homeRepository.loadCurrentTask()
expectThat(result).data.isEqualTo(TestData.CurrentTask)
}
@Test
fun loadCurrentTask_loadsCurrentTaskNetwork() = testScope.runTest {
val homeRepository = createRepository()
homeRepository.loadCurrentTask()
runCatching {
val request = mockWebServer.takeRequest(1, TimeUnit.SECONDS)
expectThat(request).hasRequestLine("GET /schedule?size=1")
}
}
@Test
fun loadCurrentTask_insertsCurrentTaskInDatabase() = testScope.runTest {
val homeRepository = createRepository()
homeRepository.loadCurrentTask()
val schedule = promptDatabase.taskDao().getSchedule().first()
expectThat(schedule).containsExactly(TestData.CurrentTask)
}
private fun createRepository(): HomeRepository {
val promptService = createPromptService(mockWebServer.url("/"))
return HomeRepository(testDispatcher, promptDatabase, promptService)
}
}
这让我完成了 90% 的事情。直接测试 currentTask
属性 还是不错的,因为那里还剩下一点逻辑。
我运行遇到了同样的问题。我发现这一切都是因为 kotlin-coroutines-test 1.6.0,更具体地说是 运行 测试行为。
运行Test 允许您控制虚拟时间(如 运行BlockingTest),这在测试时很方便。但是你这里的问题是你正在使用 OkHttp 的 Retrofit(测试中的 MockWebServer),它 运行 在它自己的线程内并且在测试调度程序之外使用实时。
这里的解决方案是
- 使用运行阻塞而不是运行测试是否不需要控制虚拟时间,例如调用 delay()
- 运行 通过使用“withContext(Dispatchers.Default)”或“withContext(Dispatchers.IO)” 包装您的测试内容,您在另一个调度程序上的测试
@Test
fun `my unit test`() = runTest {
withContext(Dispatchers.Default) { // can be either Dispatchers.Default or Dispatchers.IO but not Dispatchers.Main
// enter code here
}
}
另一种解决方案是通过在测试 class 中创建 属性 来为主调度程序提供不同的线程上下文,例如:
@OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
@Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
}
@After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
@Test
fun `my unit test`() = runTest {
withContext(Dispatchers.Main) { // this is not mandatory here as the default dispatcher is Main
// enter code here
}
}
我对协同程序和协同程序测试还很陌生,所以我可能误解了一些细节,但我希望它能有所帮助。
文档也可以派上用场:https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md