如何在CoroutineScope中使用MockWebServer成功Mock Retrofit调用?
问题分析
你遇到的核心问题是:ViewModel里用CoroutineScope(Dispatchers.IO).launch创建的协程,脱离了测试用runTest调度器的控制——测试方法执行完毕时,协程还没来得及执行execute()代码块,导致你看不到运行结果。而去掉CoroutineScope时,代码在主线程同步执行,自然能正常跑完。
解决方案
下面提供两种可行的解决方式,按需选择:
方案1:给ViewModel注入CoroutineScope(推荐)
业务代码里不要硬编码创建CoroutineScope,而是通过构造函数注入,这样测试时可以传入受测试调度器管理的作用域,从根源上让协程受测试控制。
修改ViewModel代码
class YourViewModel( private val sensorsService: SensorsService, // 注入协程作用域,默认用Android官方推荐的viewModelScope private val coroutineScope: CoroutineScope = viewModelScope ) : ViewModel() { fun loadSensors() { coroutineScope.launch(Dispatchers.IO) { sensorsService.getUserSensors("", "") .execute().let { println("inside getUserSensors()") if (it.isSuccessful) { loadingDialog.postValue(Event(false)) consumeLoadedSensors(it) } else { loadingDialog.postValue(Event(false)) showLoadSensorsFailure() } } } } }
修改测试代码
在测试中创建使用TestDispatcher的CoroutineScope,注入到ViewModel:
@Test fun `Loads sensors`() = runTest { val testScope = CoroutineScope(testDispatcher) // runTest自带的测试调度器 val viewModel = YourViewModel(sensorsService, testScope) val mDispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { val sensorsRegex = Regex("/") return if (request.path?.matches(sensorsRegex) == true) { MockResponse().setBody("[1]") } else { MockResponse().setResponseCode(500) } } } mockWebServer.dispatcher = mDispatcher viewModel.loadSensors() // 协程在testDispatcher上运行,runTest会自动等待所有协程执行完成 }
方案2:替换Dispatchers.IO为TestDispatcher(适合快速调试)
如果不想修改ViewModel的构造,可以在测试中替换全局的Dispatchers.IO为测试调度器,让ViewModel里的协程在可控环境执行:
修改测试类的初始化/销毁逻辑
@OptIn(ExperimentalCoroutinesApi::class) @BeforeAll fun beforeAll() { Dispatchers.setMain(mainThreadSurrogate) // 把IO调度器替换为测试调度器 Dispatchers.setIo(testDispatcher) } @OptIn(ExperimentalCoroutinesApi::class) @AfterAll fun afterAll() { Dispatchers.resetMain() Dispatchers.resetIo() // 重置IO调度器,避免影响其他测试 mainThreadSurrogate.close() }
测试方法无需修改
这样ViewModel里的CoroutineScope(Dispatchers.IO)会自动使用测试调度器,runTest会等待协程执行完毕,execute()代码块就能正常运行了。
额外建议
Retrofit的execute()是同步阻塞方法,在协程里更推荐使用挂起函数(定义Retrofit接口时用suspend fun),既避免阻塞线程,测试也更友好:
// 改造SensorsApi接口为挂起函数 interface SensorsApi { @GET("/") suspend fun getUserSensors(@Query("param1") p1: String, @Query("param2") p2: String): Response<List<Int>> } // ViewModel里调用方式简化 coroutineScope.launch(Dispatchers.IO) { val response = sensorsService.getUserSensors("", "") // 后续逻辑... }
内容的提问来源于stack exchange,提问作者Captain Jacky
相关产品推荐
相关产品推荐

