0

I am trying to write class which will get some current data every some time so it will transform one flow into another. I go stuck while writing tests for it.

When I run one test it passes but when I run all of them: testA always passes but testB or testC fails with this error:

kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs

Classes:

interface Time {
    val currentTime: Flow<Int>
}

data class Data(val value: Int)

interface DataRepository {
    fun getCurrentData(): Data
}

My main class:

@Singleton
class CurrentDataProvider(time: Time, repository: DataRepository) {
    private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    private var lastUpdate = Int.MIN_VALUE

    val currentData: SharedFlow<Data> =
        time.currentTime
            .transform {
                if (lastUpdate + 10 <= it) {
                    lastUpdate = it
                    emit(repository.getCurrentData())
                }
            }
            .onCompletion {
                coroutineScope.cancel() // Is it necessary?
            }
            .shareIn(
                scope = coroutineScope,
                started = SharingStarted.Eagerly,
            )
}

And tests:

class Test {
    private val time = mock<Time>()
    private val data = Data(123)
    private val repository = mock<DataRepository>{
        on { getCurrentData() } doReturn data
    }

    @Test
    fun `testA`() = runTest {
        // given
        whenever(time.currentTime).doReturn(flowOf(0))

        // when
        val tested = CurrentDataProvider(time, repository)

        // then
        tested.currentData.test {
            val item = awaitItem()
            item shouldBe data
        }
        verify(repository, times(1)).getCurrentData()
    }

    @Test
    fun `testB`() = runTest {
        // given
        whenever(time.currentTime).doReturn(flowOf(0, 1, 2, 3))

        // when
        val tested = CurrentDataProvider(time, repository)

        // then
        tested.currentData.test {
            val item = awaitItem()
            item shouldBe data
        }
        verify(repository, times(1)).getCurrentData()
    }

    @Test
    fun `testC`() = runTest {
        // given
        whenever(time.currentTime).doReturn(flowOf(1, 2, 3, 11))

        // when
        val tested = CurrentDataProvider(time, repository)

        // then
        tested.currentData.test {
            awaitItem()
            val item = awaitItem()
            item shouldBe data
        }
        verify(repository, times(2)).getCurrentData()
    }

I tried to:

  • add withContext(Dispatchers.Default)
  • add MainDispatcherRule:
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

And it still fails every ~5th test.

gabilcious
  • 38
  • 5

1 Answers1

1

Try injecting the coroutine scope into the constructor of CurrentDataProvider instead of initializing it as a field variable.

Test a could be written like

@Test
fun `testA`() = runTest {
    val testScope = TestCoroutineScope()
    whenever(time.currentTime).doReturn(flowOf(0))

    val tested = CurrentDataProvider(time, repository, testScope)

    tested.currentData.test {
        val item = awaitItem
        item shouldBe data
    }
    verify(repository, times(1)).getCurrentData()
    testScope.cleanupTestCoroutines() // Add this line to clean up the test scope
}

Also, checkout SonarLint plugin which can find code smells like field initialized coroutineScopes instead of constructor-injected

Sean Blahovici
  • 5,350
  • 4
  • 28
  • 38
  • `TestCoroutineScope` is deprecated, but when I just passed `this` it worked. The problem is that I wanted it to be independent running Singleton, so I don't want to give it `CoroutineScope` of any single `ViewModel` and make it dependent on it. What is best solution then? Does it make sense to provide it in module (Hilt) and inject `CurrentDataCoroutineScopeHolder` to `CurrentDataProvider`? – gabilcious Apr 01 '23 at 22:19
  • `@JvmStatic @Provides @Singleton fun provideCurrentDataCoroutineScopeHolder(): CurrentDataCoroutineScopeHolder = CurrentDataCoroutineScopeHolder(CoroutineScope(SupervisorJob() + Dispatchers.Default))` – gabilcious Apr 01 '23 at 22:19