You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含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)

如果项目已使用依赖注入框架,可通过限定符绑定调度器,让框架自动管理单例与调度器注入,完全无需手动处理线程安全问题。

实现步骤

  1. 定义调度器限定符:
import javax.inject.Qualifier
import kotlin.annotation.AnnotationRetention.BINARY

@Qualifier
@Retention(BINARY)
annotation class IoDispatcher
  1. 生产环境绑定调度器:
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
}
  1. 改造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()
    }
}
  1. 测试环境替换调度器:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 16:10:23