使用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
相关产品推荐
相关产品推荐

