Eva*_*Eva 5 android kotlin mockwebserver retrofit2 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$1$1.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
(Coroutine boundary)
at app.cash.turbine.ChannelBasedFlowTurbine$awaitEvent$2.invokeSuspend(FlowTurbine.kt:247)
at app.cash.turbine.ChannelBasedFlowTurbine$withTimeout$2.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$1$1.invokeSuspend(HomeRepositoryTest.kt:90)
at app.cash.turbine.FlowTurbineKt$test$2.invokeSuspend(FlowTurbine.kt:86)
at ogbe.eva.prompt.home.HomeRepositoryTest$currentTask when server responds with error emits failure$1.invokeSuspend(HomeRepositoryTest.kt:89)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invokeSuspend(TestBuilders.kt:208)
(Coroutine creation stacktrace)
at app//kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:184)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.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$1.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$1.evaluate(TestWatcher.java:61)
at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at app//org.junit.runners.BlockJUnit4ClassRunner$1.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$4.run(ParentRunner.java:331)
at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at app//org.junit.runners.ParentRunner$3.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$2.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$1$1.invokeSuspend(TestBuilders.kt:167)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.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
Run Code Online (Sandbox Code Playgroud)
我没有机会运行我的测试断言,而这正是我想做的。它只是因这个意外错误而失败。
我已经检查了在流程中调用随机挂起函数并在流程函数之外运行模拟服务器。这两个操作都将完成,不会出现超时错误,但是当我将流程、测试和改造结合起来时,它会显示超时错误。
存储库代码:
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
}
}
Run Code Online (Sandbox Code Playgroud)
测试代码:
@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)
}
}
Run Code Online (Sandbox Code Playgroud)
规则代码:
@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)
}
Run Code Online (Sandbox Code Playgroud)
如何测试使用 Retrofit 的流程而不出现超时错误?
小智 1
我遇到了同样的问题。我发现这一切都是因为 kotlin-coroutines-test 1.6.0,更具体地说是 runTest 行为。
runTest 允许您控制虚拟时间(如 runBlockingTest),这在测试时很方便。但这里的问题是,您将 Retrofit 与 OkHttp(测试中的 MockWebServer)一起使用,它在自己的线程内运行,并且在测试调度程序之外实时使用。
这里的解决方案是
@Test
fun `my unit test`() = runTest {
withContext(Dispatchers.Default) { // can be either Dispatchers.Default or Dispatchers.IO but not Dispatchers.Main
// enter code here
}
}
Run Code Online (Sandbox Code Playgroud)
另一个解决方案是通过在测试类中创建一个属性来为主调度程序提供不同的线程上下文,例如:
@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
}
}
Run Code Online (Sandbox Code Playgroud)
我对协程和协程测试非常陌生,所以我可能误解了一些细节,但我希望它能有所帮助。
该文档也很方便:https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md