Kotlin协程结合Retrofit的PriceService带Mock单元测试编写求助
PriceService 单元测试实现方案
前置依赖配置
需要先在测试依赖中引入以下组件:
- 协程测试库:
kotlinx-coroutines-test:1.7.3(版本和项目协程版本保持一致即可) - Mock框架:推荐用MockK,依赖为
io.mockk:mockk:1.13.8,也可以替换为Mockito,本文示例用MockK实现 - Junit基础测试依赖:
junit:junit:4.13.2(Junit5对应调整配置即可)
业务类优化说明
原有PriceService硬编码了APIClient初始化逻辑,无法直接Mock APIInterface,先做最小改造,不影响原有线上逻辑:
class PriceService( // 构造函数注入APIInterface,默认保持原有初始化逻辑,不影响线上调用 private val apiInterface: APIInterface? = APIClient.getClient()?.create(APIInterface::class.java) ) { fun main(priceCallback: PriceServiceCallBack) { val exceptionHandler = CoroutineExceptionHandler { _, throwable -> println("Caught $throwable") // 可按需补全异常时的回调逻辑 } CoroutineScope(Dispatchers.IO).launch(exceptionHandler) { val response = apiInterface?.prices() withContext(Dispatchers.Main) { if (response!!.isSuccessful) { priceCallback.getData(response.body()) } else { onError("Error : ${response.message()} ") } } } } // 原有onError方法保持不变 private fun onError(msg: String) { // 原有业务逻辑 } }
你提到的Retrofit无法调用enqueue/execute的问题,因为当前代码里prices()是Retrofit声明的suspend挂起方法,直接返回Response对象,所以单元测试里不需要调用这两个方法,直接Mock挂起方法的返回值即可。
测试用例实现
首先实现协程调度器替换的测试规则,避免单元测试找不到Android主线程的问题:
@OptIn(ExperimentalCoroutinesApi::class) class CoroutineTestRule : TestWatcher() { val testDispatcher = StandardTestDispatcher() override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } }
正式测试类实现:
@OptIn(ExperimentalCoroutinesApi::class) class PriceServiceTest { // 注入协程测试规则,替换调度器 @get:Rule val coroutineRule = CoroutineTestRule() // Mock依赖对象 private val mockApi = mockk<APIInterface>() private val mockCallback = mockk<PriceServiceCallBack>() // 测试用PriceService实例,传入Mock的API接口 private val priceService = PriceService(mockApi) @Test fun `请求成功时回调正确返回数据`() = runTest { // 1. 构造模拟返回数据,替换为你实际的Price数据结构 val mockPriceData = listOf("100", "200", "300") val mockResponse = Response.success(mockPriceData) // 2. Mock API接口的prices方法返回值 coEvery { mockApi.prices() } returns mockResponse // 3. Mock回调方法处理 every { mockCallback.getData(any()) } just Runs // 4. 执行测试方法 priceService.main(mockCallback) // 推进协程执行直到所有任务完成 coroutineRule.testDispatcher.scheduler.advanceUntilIdle() // 5. 验证调用符合预期 coVerify(exactly = 1) { mockApi.prices() } verify(exactly = 1) { mockCallback.getData(mockPriceData) } } @Test fun `请求失败时触发错误逻辑`() = runTest { // 1. 构造失败响应 val mockErrorResponse = Response.error<String>(400, "请求参数错误".toResponseBody("text/plain".toMediaType())) // 2. Mock接口返回失败 coEvery { mockApi.prices() } returns mockErrorResponse as Response<*> // 如果onError方法会调用callback的错误回调,此处补充Mock逻辑即可 // 3. 执行测试 priceService.main(mockCallback) coroutineRule.testDispatcher.scheduler.advanceUntilIdle() // 4. 验证逻辑 coVerify(exactly = 1) { mockApi.prices() } // 按需补充错误逻辑的验证 } @Test fun `接口抛出异常时触发异常捕获`() = runTest { // 1. Mock接口调用抛出异常 coEvery { mockApi.prices() } throws RuntimeException("网络异常") // 2. 执行测试 priceService.main(mockCallback) coroutineRule.testDispatcher.scheduler.advanceUntilIdle() // 3. 验证异常被正常捕获 coVerify(exactly = 1) { mockApi.prices() } } }
兼容说明
- 如果使用旧版本协程测试库,把
runTest替换为runBlocking,调度器推进逻辑对应调整即可 - 如果用Mockito作为Mock框架,把MockK的
coEvery/coVerify替换为Mockito的whenever/verify,搭配Mockito-Kotlin实现协程方法Mock即可 - 原代码中
response!!.isSuccessful的非空断言遇到response为null时会崩溃,建议补充空判断优化逻辑
内容的提问来源于stack exchange,提问作者Mohamad Rostami
相关产品推荐
相关产品推荐

