Kotlin中Mock对象时调试与实际测试结果不一致问题排查
问题:Mockk模拟Companion对象时调试正常但运行测试触发真实方法
我用Mockk编写测试用例模拟对象,遇到一个奇怪的问题:在IntelliJ调试测试时,调试窗口能获取到Mock响应,但完整运行测试时,却出现数据库客户端错误(实际执行了真实的数据库方法)。
服务代码
class SendMessageService ( private val authHelper: AuthHelper ) { companion object { val targetsDb = TargetsDynamodbService() } fun handlePushIdRequest(sendMessageRequest: SendMessageRequest): Observable<List<Target>> { if (!validAuthorization(sendMessageRequest.serviceKey)) { throw ClientErrorException("Invalid Authorization", Response.Status.UNAUTHORIZED) } return targetsDb.getAllTargetsAsObservable(sendMessageRequest.pushId) } private fun validAuthorization(serviceKey: ServiceKey): Boolean { val authorization = authHelper.getTargetId("service key") return authorization == Configuration.SendMessageAuthorization.authorization(serviceKey) } }
测试用例代码
class SendMessageServiceTest : AbstractServiceTest() { private val authHelper = AuthHelper(listOf("abc")) private val targetsDynamodbService: TargetsDynamodbService = mockk() private lateinit var service: SendMessageService @BeforeEach fun setUp() { mockkObject(SendMessageService.Companion) every { SendMessageService.Companion.targetsDb } returns targetsDynamodbService ConfigurationManager.getConfigInstance().setProperty("sendmessage.authorization.test", "abc") } @Test fun `successfully send pushId request message`() { every { targetsDynamodbService.getAllTargetsAsObservable(any()) } answers { Observable.just(listOf(Target().apply { pushId = arg<String>(0) })) } service = SendMessageService(authHelper) val response = service.handlePushIdRequests(createMessageRequest()).blockingFirst() assertTrue(response.isNotEmpty()) } private fun createMessageRequest() = SendMessageRequest( pushId = "pushId", serviceKey = ServiceKey.TEST, title = TextLocalization("en", "nl"), body = TextLocalization("en", "nl"), silent = false, collapseKey = null, data = null, androidOptions = null, iosOptions = null ) }
问题原因与修复方案
核心原因
问题出在Companion对象中targetsDb的初始化时机与Mockk的模拟时机不匹配:
SendMessageService.Companion里的targetsDb是val类型成员变量,在类加载阶段就会初始化真实的TargetsDynamodbService实例。- 测试中
setUp方法才执行Mock替换操作,非调试模式下测试运行速度快,替换操作还没完成,业务代码就已经引用了真实的数据库实例;而调试时因为断点停顿,替换操作完成后才执行业务逻辑,所以Mock能生效。
修复方案
方案1:延迟初始化Companion中的targetsDb
修改SendMessageService的Companion对象,用lazy延迟初始化targetsDb,确保第一次调用时才创建实例,此时Mock替换已经完成:
companion object { val targetsDb by lazy { TargetsDynamodbService() } }
方案2:调整Service实例的创建时机
把service = SendMessageService(authHelper)移到setUp方法中,确保Mock替换完成后再创建Service实例:
@BeforeEach fun setUp() { mockkObject(SendMessageService.Companion) every { SendMessageService.Companion.targetsDb } returns targetsDynamodbService ConfigurationManager.getConfigInstance().setProperty("sendmessage.authorization.test", "abc") service = SendMessageService(authHelper) // 移至此处 } @Test fun `successfully send pushId request message`() { every { targetsDynamodbService.getAllTargetsAsObservable(any()) } answers { Observable.just(listOf(Target().apply { pushId = arg<String>(0) })) } // 注意:原测试方法中的handlePushIdRequests是笔误,对应服务方法应为handlePushIdRequest val response = service.handlePushIdRequest(createMessageRequest()).blockingFirst() assertTrue(response.isNotEmpty()) }
方案3:改用构造注入(推荐)
这是更优雅的解耦方式,将targetsDb作为构造参数传入SendMessageService,彻底避免静态成员带来的Mock问题:
class SendMessageService ( private val authHelper: AuthHelper, // 提供默认实现,不影响原有业务代码使用 private val targetsDb: TargetsDynamodbService = TargetsDynamodbService() ) { fun handlePushIdRequest(sendMessageRequest: SendMessageRequest): Observable<List<Target>> { if (!validAuthorization(sendMessageRequest.serviceKey)) { throw ClientErrorException("Invalid Authorization", Response.Status.UNAUTHORIZED) } return targetsDb.getAllTargetsAsObservable(sendMessageRequest.pushId) } private fun validAuthorization(serviceKey: ServiceKey): Boolean { val authorization = authHelper.getTargetId("service key") return authorization == Configuration.SendMessageAuthorization.authorization(serviceKey) } }
测试时直接传入Mock实例,无需再Mock Companion对象:
@BeforeEach fun setUp() { ConfigurationManager.getConfigInstance().setProperty("sendmessage.authorization.test", "abc") service = SendMessageService(authHelper, targetsDynamodbService) }
内容的提问来源于stack exchange,提问作者MrAndre
相关产品推荐
相关产品推荐

