如何为含init块调度器的Kotlin单例注入测试协程依赖?
解决Kotlin单例类调度器注入的无竞态方案
针对你遇到的单例SomeRepository在init块中使用固定调度器导致测试异常的问题,以下是几种无需变量Setter、无竞态的实现方案:
方案1:可控单例+构造器传入调度器
将原object单例改为私有构造器+伴生对象线程安全单例,在创建实例时直接传入调度器,从根源避免竞态(因为调度器在实例初始化时就确定,无后续修改)。
实现代码
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.Dispatchers class SomeRepository private constructor( private val dispatcher: CoroutineDispatcher, // 推荐用自定义Scope替代GlobalScope,便于测试时控制生命周期 private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) ) { init { // 使用注入的dispatcher执行协程逻辑 coroutineScope.launch { // 你的业务代码 } } // 测试时可调用此方法取消协程,避免资源泄漏 fun cleanup() { coroutineScope.cancel() } companion object { @Volatile private var INSTANCE: SomeRepository? = null // 生产环境直接调用getInstance(),测试时传入测试调度器 fun getInstance( dispatcher: CoroutineDispatcher = Dispatchers.IO, customScope: CoroutineScope? = null ): SomeRepository { return INSTANCE ?: synchronized(this) { INSTANCE ?: SomeRepository( dispatcher, customScope ?: CoroutineScope(SupervisorJob() + dispatcher) ).also { INSTANCE = it } } } } }
测试用法
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.BeforeEach import org.junit.AfterEach @OptIn(ExperimentalCoroutinesApi::class) class SomeRepositoryTest { private val testDispatcher = UnconfinedTestDispatcher() private lateinit var repository: SomeRepository @BeforeEach fun setup() { // 重置单例,避免测试间污染 SomeRepository.Companion.INSTANCE = null // 传入测试调度器创建实例 repository = SomeRepository.getInstance(testDispatcher) } @AfterEach fun teardown() { repository.cleanup() } // 编写你的测试用例... }
方案2:依赖注入框架(如Hilt)
如果项目已使用依赖注入框架,可通过限定符绑定调度器,让框架自动管理单例与调度器注入,完全无需手动处理线程安全问题。
实现步骤
- 定义调度器限定符:
import javax.inject.Qualifier import kotlin.annotation.AnnotationRetention.BINARY @Qualifier @Retention(BINARY) annotation class IoDispatcher
- 生产环境绑定调度器:
import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @Module @InstallIn(SingletonComponent::class) object DispatcherModule { @IoDispatcher @Provides fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO }
- 改造Repository为Hilt单例:
import dagger.hilt.android.scopes.SingletonScoped import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @SingletonScoped class SomeRepository @Inject constructor( @IoDispatcher private val dispatcher: CoroutineDispatcher, private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) ) { init { coroutineScope.launch { // 你的业务代码 } } fun cleanup() { coroutineScope.cancel() } }
- 测试环境替换调度器:
import dagger.Module import dagger.Provides import dagger.hilt.testing.TestInstallIn import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @OptIn(ExperimentalCoroutinesApi::class) @TestInstallIn( components = [SingletonComponent::class], replaces = [DispatcherModule::class] ) object TestDispatcherModule { @IoDispatcher @Provides fun provideTestIoDispatcher(): CoroutineDispatcher = UnconfinedTestDispatcher() }
核心思路总结
- 避免在单例init块中使用硬编码的
Dispatchers.IO或GlobalScope,改为在实例创建时传入调度器和自定义Scope - 线程安全的单例创建逻辑(synchronized+@Volatile)或依赖注入框架,从根本消除竞态问题
- 自定义Scope替代GlobalScope,便于测试时控制协程生命周期,避免测试后资源泄漏
内容的提问来源于stack exchange,提问作者Alix
相关产品推荐
相关产品推荐

