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

使用TestScope+MockWebServer运行Retrofit时测试异常终止问题

问题分析与解决方案

问题原因

测试中使用TestScope调用frankfurterApi.convert时测试提前终止,核心是TestScope默认的UnconfinedTestDispatcher无法正确处理Retrofit内部的IO调度逻辑:

  • Retrofit的suspend方法默认切换到Dispatchers.IO执行网络请求
  • UnconfinedTestDispatcher不会拦截这个调度切换,导致网络请求协程在测试协程外异步执行,advanceUntilIdle()无法感知这些协程,测试直接走到断言步骤,此时网络请求尚未完成,println("here2")自然不会执行。

而runBlocking使用默认调度器能等待所有IO协程完成,生产环境的viewModelScope绑定Dispatchers.Main,配合Retrofit的IO调度器也能正常执行。

修复方案

方案1:统一测试调度器,让Retrofit使用测试调度器

在测试中指定StandardTestDispatcher,让OkHttpClient和所有协程都使用该调度器,确保所有异步操作都能被advanceUntilIdle()推进执行。

修改测试代码:

private lateinit var mockWebServer: MockWebServer
private lateinit var testDispatcher: StandardTestDispatcher

@Before
fun init() {
    mockWebServer = MockWebServer()
    auth = mockAuth()
    firestore = mockFirestore(listener)
    testDispatcher = StandardTestDispatcher()
    // 替换Main和IO调度器为测试调度器
    Dispatchers.setMain(testDispatcher)
    Dispatchers.IO.setDispatcher(testDispatcher)
}

@After
fun tearDown() {
    Dispatchers.resetMain()
    Dispatchers.IO.resetDispatcher()
    mockWebServer.shutdown()
}

@Test
fun requestConversion_success() = runTest(testDispatcher) {
    val currentBalance = 234f
    val from = CurrencyEnum.EUR
    val to = CurrencyEnum.USD
    
    // 给OkHttpClient设置使用测试调度器的Dispatcher
    val okHttpClient = OkHttpClient.Builder()
        .dispatcher(Dispatcher(testDispatcher))
        .build()
        
    val api = Retrofit.Builder()
        .baseUrl(mockWebServer.url("/"))
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(FrankfurterApi::class.java)
        
    val jsonResponse = """{
          "amount": $currentBalance,
          "base": "$from",
          "date": "2024-06-12",
          "rates": {
            "$to": 219.3
          }
        }"""
    mockWebServer.enqueue(MockResponse().setBody(jsonResponse).setResponseCode(200))
    
    val datastoreManager = mockk<DataStoreManager> {
        every { balanceFlow() } returns flowOf(Pair(from.ordinal, currentBalance))
        every { themeFlow() } returns flowOf()
    }
    
    val repository = SettingsRepository(auth, firestore.collection("data"), api, datastoreManager)
    val viewModel = SettingsViewModel(repository, CoroutineScopeProvider(this))
    
    // 确保balanceFlow的数据被收集到_uiState
    advanceUntilIdle()
    
    viewModel.changeCurrency(to)
    advanceUntilIdle()
    
    println(viewModel.uiState.value.currencyChangeResult)
    assertTrue(viewModel.uiState.value.currencyChangeResult is Result.Success)
}

方案2:绑定ViewModel的Scope到TestScope调度器

如果不想修改OkHttpClient,可强制ViewModel的Scope使用TestScope的调度器,同时拦截Retrofit的IO操作:

// 修改ViewModel的scope初始化逻辑
private val scope = coroutineScope ?: viewModelScope.plus(testDispatcher)

但更推荐方案1的全局调度器替换,能确保所有协程逻辑在测试环境下被统一控制。

额外检查点

确保ViewModel正确收集balanceFlow的数据,否则_uiState.value.balance可能为默认值导致逻辑异常,可在ViewModel中添加:

init {
    viewModelScope.launch {
        dataStoreManager.balanceFlow().collect { (currencyOrdinal, balance) ->
            _uiState.update { it.copy(balance = Balance(currencyOrdinal, balance)) }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 14:12:32