[FIXED] Datastore instrumented test fails with kotlinx.coroutines.test.UncompletedCoroutinesError

Issue

Running Datastore instrumented test fails with
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: [ScopeCoroutine{Active}@e24f0d2]

So far Tried to:

  1. Use runBlockingTest or runBlocking instead of runTest
  2. Tried to call runBlocking/runBlockingTest and runTest by the testDispatcher
  3. Changed the context of runTest
  4. Launched the collection of flow in test and consumed it before finally cancelling the job
  5. Tried to use Flow Terminal operators (e.g. first(), take()) to test flow instead of turbine test

TestAppModule

@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [LocalModule::class])
object TestAppModule {

    //...

    @OptIn(ExperimentalCoroutinesApi::class)
    @Singleton
    @Provides
    fun provideFakePreferences(
        @ApplicationContext context: Context,
    ): DataStore<Preferences> {

        return PreferenceDataStoreFactory
            .create(
                scope = TestScope(),
                produceFile = {
                    context.preferencesDataStoreFile("test_pref_file")
                }
            )
    }

}

RepoPreferencesTest

@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class RepoPreferencesTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var testDataStore: DataStore<Preferences>

    lateinit var repoPreferences: RepoPreferences

    private val testCoroutineDispatcher = UnconfinedTestDispatcher()

    private val testCoroutineScope = TestScope(testCoroutineDispatcher + Job())

    @Before
    fun setup() {
        hiltRule.inject()

        repoPreferences = RepoPreferencesImpl(testDataStore)
    }


    @Test
    fun getInitialLanguageFromInitially_ReturnsDefaultValue() = runTest {
        repoPreferences.language.test {
            assertThat(awaitItem()).isEqualTo(RemoteConstants.DEFAULT_LANGUAGE)
            cancelAndIgnoreRemainingEvents()
        }
    }

    //...
}

Solution

The problem is probably with the EmptyCoroutineContext that TestScope uses by default.

I have overridden the context of TestScope with UnconfinedTestDispatcher (which launches coroutines eagerly) and a Job and passed that scope to the PreferenceDataStoreFactory instead:

CoroutineTestModule

@OptIn(ExperimentalCoroutinesApi::class)
@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [CoroutineModule::class])
object CoroutineTestModule {
    @Provides
    @Singleton
    fun provideTestCoroutineScope(): CoroutineScope {
        val testCoroutineDispatcher = UnconfinedTestDispatcher()
        return TestScope(testCoroutineDispatcher + Job())
    }
}

TestAppModule

@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [LocalModule::class])
object TestAppModule {

    //...

    @Singleton
    @Provides
    fun provideFakePreferences(
        @ApplicationContext context: Context,
        scope: CoroutineScope
    ): DataStore<Preferences> {

        return PreferenceDataStoreFactory
            .create(
                scope = scope,
                produceFile = {
                    context.preferencesDataStoreFile("test_pref_file")
                }
            )
    }

 }

Answered By – Astro

Answer Checked By – Robin (FixeMe Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *