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

Kotlin协程ViewModel单元测试失效,主调度器初始化失败

ViewModel单元测试报错:主调度器模块初始化失败

问题背景

原ViewModel单元测试方案失效,为使viewModelScope.launch创建的协程正常运行,已按照官方文档实现MainDispatcherRule重置主调度器为测试调度器,但测试编译通过后运行失败,报错主调度器模块初始化失败。

相关代码

MainDispatcherRule实现

@ExperimentalCoroutinesApi
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {

    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

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

RestaurantsViewModel代码

@HiltViewModel
class RestaurantsViewModel @Inject constructor(private val repo: RestaurantsRepository): ViewModel() {

    private val _restaurantsState = MutableLiveData<ResultState<List<Restaurant>>>()
    val restaurantsState: LiveData<ResultState<List<Restaurant>>> = _restaurantsState


    init {
        initialize()
    }

    fun initialize() {
        viewModelScope.launch {
            _restaurantsState.postValue(ResultState.Loading)
            try {
                _restaurantsState.postValue(
                    ResultState.Success(repo.getRestaurants())
                )
            } catch (ex: Exception) {
                _restaurantsState.postValue(
                    ResultState.Failure("Failed to fetch restaurants", ex)
                )
            }
        }
    }
}

测试类代码(原错误版本)

@ExperimentalCoroutinesApi
class RestaurantsViewModelTest {

    private var repository = mockk<RestaurantsRepository>(relaxed = true)
    private var viewmodel = RestaurantsViewModel(repository)

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun `restaurantsState LiveData is updated with the result from repository`() = runTest {
        val restaurants = listOf(mockk<Restaurant>(relaxed = true))
        val success = ResultState.Success(restaurants)
        coEvery { repository.getRestaurants() } returns restaurants

        viewmodel.initialize()

        Assert.assertEquals(success, viewmodel.restaurantsState.value)
    }
}

项目依赖配置

dependencies {

implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.activity:activity-ktx:1.6.0"
implementation "androidx.fragment:fragment-ktx:1.5.3"

// testing
// for runTest, CoroutineDispatcher
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
// for InstantTaskExecutorRule
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "io.mockk:mockk:1.13.2"

implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}

报错信息

Exception in thread "Test worker" java.lang.IllegalStateException: 主调度器对应的模块初始化失败。测试环境下可使用kotlinx-coroutines-test模块中的Dispatchers.setMain方法解决

问题原因

测试类中viewmodel在类初始化阶段就被实例化,此时MainDispatcherRule的starting方法尚未执行(JUnit4规则的执行顺序是:Rule的starting → @Before方法 → 测试方法),ViewModel的init块会立即触发initialize()方法,调用viewModelScope.launch,此时主调度器未被替换为测试调度器,导致报错。

解决方案

1. 延迟ViewModel实例化时机

将ViewModel的初始化移至@Before注解的方法中,确保MainDispatcherRule已完成主调度器的设置后再创建ViewModel。

2. 确保协程执行完毕后再断言

在runTest中调用advanceUntilIdle(),等待所有测试调度器中的协程执行完成,保证LiveData的value已更新。

修改后的测试类代码

@ExperimentalCoroutinesApi
class RestaurantsViewModelTest {

    private lateinit var repository: RestaurantsRepository
    private lateinit var viewmodel: RestaurantsViewModel

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setup() {
        repository = mockk(relaxed = true)
        // 此时MainDispatcherRule已完成主调度器设置
        viewmodel = RestaurantsViewModel(repository)
    }

    @Test
    fun `restaurantsState LiveData is updated with the result from repository`() = runTest {
        val restaurants = listOf(mockk<Restaurant>(relaxed = true))
        val success = ResultState.Success(restaurants)
        coEvery { repository.getRestaurants() } returns restaurants

        viewmodel.initialize()
        // 等待所有协程执行完毕
        advanceUntilIdle()

        Assert.assertEquals(success, viewmodel.restaurantsState.value)
    }
}

内容的提问来源于stack exchange,提问作者HukeLau_DABA

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 11:52:43