简体   繁体   中英

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

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?

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.

runTest allows you to control the virtual time (like runBlockingTest) which is convenient when testing. 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.

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
  • run your tests on another dispatcher by wrapping your test content with "withContext(Dispatchers.Default)" or "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:

    @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

I still haven't figured out how to directly test the currentTask flow property. 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.

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. It would still be nice to directly test the currentTask property since there is a little bit of logic still left in there.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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