简体   繁体   English

在 Kotlin 流程中为 Retrofit 协程运行测试时出现 TimeoutCancellationException

[英]TimeoutCancellationException when running tests for a Retrofit coroutine in a Kotlin flow

I have a repository that creates a flow where I emit the result of a suspending Retrofit method.我有一个创建流的存储库,我在其中发出暂停 Retrofit 方法的结果。 This works in the app, but I would like to run tests on the code.这在应用程序中有效,但我想对代码运行测试。

I am using kotlinx-coroutines-test v1.6.0 and MockWebServer v4.9.3 in my tests.我在测试中使用 kotlinx-coroutines-test v1.6.0 和 MockWebServer v4.9.3。 When I try to run a test, I get:当我尝试运行测试时,我得到:

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

I don't get the chance to run my test assertions, which is what I want to do.我没有机会运行我的测试断言,而这正是我想要做的。 It just fails with this unexpected error.它只是因这个意外错误而失败。

I have checked with calling random suspending functions in the flow and running the mock server outside of my flow function. Both of those will complete without the timeout error, but when I combine the flow and the test and Retrofit, it shows the timeout error.我已经检查了在流程中调用随机挂起函数并在我的流程 function 之外运行模拟服务器。这两个都将在没有超时错误的情况下完成,但是当我将流程和测试以及 Retrofit 结合时,它显示超时错误。

Repository code:存储库代码:

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
    }
}

Test code:测试代码:

@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)
    }
}

Rule code:规则代码:

@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)
}

How do I test a flow that uses Retrofit without getting a timeout error?如何在不出现超时错误的情况下测试使用 Retrofit 的流?

I ran into the same issue.我遇到了同样的问题。 I figured out all this was because of kotlin-coroutines-test 1.6.0 and more specifically the runTest behavior.我发现这一切都是因为 kotlin-coroutines-test 1.6.0,更具体地说是 runTest 行为。

runTest allows you to control the virtual time (like runBlockingTest) which is convenient when testing. runTest 允许您控制虚拟时间(如 runBlockingTest),这在测试时很方便。 But your issue here is that you are using Retrofit with OkHttp (MockWebServer in tests) which is running inside its own thread and is using real time outside of the test dispatcher.但是你的问题是你正在使用 Retrofit 和 OkHttp(测试中的 MockWebServer),它在它自己的线程内运行并且在测试调度程序之外使用实时。

Here the solution would be to either这里的解决方案是

  • use runBlocking instead of runTest if you don't need to control the virtual time such as calls to delay() for instance如果您不需要控制虚拟时间,例如调用 delay(),请使用 runBlocking 而不是 runTest
  • run your tests on another dispatcher by wrapping your test content with "withContext(Dispatchers.Default)" or "withContext(Dispatchers.IO)"通过使用“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
        }
    }

Another solution would be to provide a different thread context to the Main dispatcher by creating a property in your test class like:另一种解决方案是通过在测试 class 中创建一个属性来为 Main 调度程序提供不同的线程上下文,例如:

    @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
        }
    }

I am pretty new to coroutines and coroutines testing specifically so I may have misunderstood some details but I hope it helped somehow.我对协同程序和协同程序测试还很陌生,所以我可能误解了一些细节,但我希望它能以某种方式有所帮助。

The documentation can also be handy: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md文档也很方便: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md

I still haven't figured out how to directly test the currentTask flow property.我仍然没有想出如何直接测试currentTask流属性。 But I did find a workaround that lets me test most of the functionality.但我确实找到了一种解决方法,可以让我测试大部分功能。

I split off a base repository that handles most of the logic without referencing Retrofit. I also split off a suspending method from the property flow, so that I can test the Retrofit logic without going through a flow.我在不引用 Retrofit 的情况下拆分了一个处理大部分逻辑的基本存储库。我还从属性流中拆分了一个挂起方法,这样我就可以在不通过流程的情况下测试 Retrofit 逻辑。

Base repository:基础仓库:

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
    }
}

Concrete repository:具体存储库:

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
        )
}

Base repository test:基础存储库测试:

@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)
    }
}

Concrete repository test:具体存储库测试:

@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)
    }
}

This gets me 90% of the way there.这让我完成了 90% 的工作。 It would still be nice to directly test the currentTask property since there is a little bit of logic still left in there.直接测试currentTask属性还是不错的,因为那里还剩下一些逻辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM