升级androidx.datastore至1.1.1后Instrumentation测试报UncompletedCoroutinesError
DataStore升级至1.1.1后Instrumentation测试阻塞失败问题修复
问题背景
将app/build.gradle中DataStore相关依赖从1.0.0升级至1.1.1:
implementation "androidx.datastore:datastore-preferences:$androidXDatastore" implementation "androidx.datastore:datastore-core:$androidXDatastore" implementation "androidx.datastore:datastore-preferences-core:$androidXDatastore"
生产代码运行正常,但Instrumentation测试抛出UncompletedCoroutinesError:
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 10s, the test coroutine is not completing, there were active child jobs: ["coroutine#1":StandaloneCoroutine{Active}@284ce4db] at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$2$1.invoke(TestBuilders.kt:349) at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$2$1.invoke(TestBuilders.kt:333) at kotlinx.coroutines.InvokeOnCancelling.invoke(JobSupport.kt:1431) at kotlinx.coroutines.JobSupport.notifyCancelling(JobSupport.kt:1477) at kotlinx.coroutines.JobSupport.tryMakeCancelling(JobSupport.kt:799) at kotlinx.coroutines.JobSupport.makeCancelling(JobSupport.kt:759) at kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:675) at kotlinx.coroutines.JobSupport.cancelCoroutine(JobSupport.kt:662) at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:159) at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:501) at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280) at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:109) at java.base@18.0.1/java.lang.Thread.run(Thread.java:833)
目标类代码:
class ScrollPositionDataStore(private val dataStore: DataStore<Preferences>) { private val _displayedPositionOffsetKey = stringPreferencesKey("displayed_position_offset") suspend fun saveScrollPositionStore(topRowIndex: Int, positionOffset: Int) { val position = "$topRowIndex,$positionOffset" dataStore.edit { preferences -> preferences[_displayedPositionOffsetKey] = position } } suspend fun getScrollPosition(): Pair<Int, Int>? = scrollPositionFlow.firstOrNull() private val scrollPositionFlow: Flow<Pair<Int, Int>> = dataStore.data .map { preferences -> // On the first run of the app, we will use LinearLayoutManager by default val i = preferences[_displayedPositionOffsetKey] val split = i?.split(",") if (split == null) Pair(4, 0) else Pair(split[0].toInt(), split[1].toInt()) } }
测试类及基类代码:
private const val POSITION_PREFERENCES_NAME = "position_preferences" @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) class ScrollPositionDataStoreTest : JunitDataStoreTest(POSITION_PREFERENCES_NAME) { private var inputDataStore: ScrollPositionDataStore = ScrollPositionDataStore(dataStore) @Test fun `ScrollPositionDataStore 入力した値をそのまま帰す`() { scope.runTest { dataStore.edit { it.clear() } inputDataStore.saveScrollPositionStore(10, 15) assertThat(inputDataStore.getScrollPosition()) .isEqualTo(Pair(10, 15)) } } @Test fun `ScrollPositionDataStore 未入力時に400を返す`() { scope.runTest { dataStore.edit { it.clear() } assertThat(inputDataStore.getScrollPosition()) .isEqualTo(Pair(4, 0)) } } @Test fun `ScrollPositionDataStore マイナス値をそのまま還す`() { scope.runTest { dataStore.edit { it.clear() } inputDataStore.saveScrollPositionStore(-1, -111) assertThat(inputDataStore.getScrollPosition()) .isEqualTo(Pair(-1, -111)) } } } @ExperimentalCoroutinesApi abstract class JunitDataStoreTest(dataStoreName :String) { private val dispatcher = UnconfinedTestDispatcher() private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext val scope = TestScope(dispatcher + Job()) val dataStore = PreferenceDataStoreFactory.create( scope = scope, produceFile = { context.preferencesDataStoreFile(dataStoreName) } ) @CallSuper @Before open fun setUp() { Dispatchers.setMain(dispatcher) } @CallSuper @After open fun tearDown() { Dispatchers.resetMain() } }
原因分析
DataStore 1.1.0及以上版本优化了协程作用域绑定逻辑,测试基类中TestScope(dispatcher + Job())的写法会导致DataStore内部协程脱离测试Scope管控。TestScope本身已自带根Job,额外添加Job()会创建独立的Job分支,测试结束时无法正确取消DataStore后台协程,进而触发UncompletedCoroutinesError。
修复方案
1. 修正测试Scope创建方式
移除TestScope初始化时额外添加的Job(),直接使用TestScope(dispatcher):
val scope = TestScope(dispatcher)
2. (可选)主动取消测试Scope
在tearDown方法中主动取消测试Scope,确保所有子协程被清理:
@CallSuper @After open fun tearDown() { Dispatchers.resetMain() scope.cancel() }
修改后的完整基类代码
@ExperimentalCoroutinesApi abstract class JunitDataStoreTest(dataStoreName :String) { private val dispatcher = UnconfinedTestDispatcher() private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext // 移除额外的Job(),使用TestScope自带的根Job val scope = TestScope(dispatcher) val dataStore = PreferenceDataStoreFactory.create( scope = scope, produceFile = { context.preferencesDataStoreFile(dataStoreName) } ) @CallSuper @Before open fun setUp() { Dispatchers.setMain(dispatcher) } @CallSuper @After open fun tearDown() { Dispatchers.resetMain() scope.cancel() } }
验证
修改后重新运行Instrumentation测试,所有测试用例应能正常通过,不再出现未完成协程的错误。
内容的提问来源于stack exchange,提问作者kou
相关产品推荐
相关产品推荐

