运行 在 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