使用@MockBean未调用thenCallRealMethod却执行真实方法问题排查
在JPA单元测试中,我用@MockBean模拟外部包的TopicV2Service<CustomMessage> Bean,没调用thenCallRealMethod(),但这个Mock Bean却执行了真实方法,抛出了NullPointerException。按道理@MockBean注册到Spring上下文后,默认方法应该返回null,请问哪里出问题了?
相关代码
MyServiceImplTest.kt
@ExperimentalCoroutinesApi @DataJpaTest @Transactional(propagation = Propagation.NOT_SUPPORTED) @ContextConfiguration(classes = [TestApplication::class]) @Import(MyServiceImpl::class) @ExtendWith(SpringExtension::class) internal class InventoryAdminApiServiceImplTest { @MockBean private lateinit var myTopicService: TopicV2Service<CustomMessage> @Suppress("SpringJavaInjectionPointsAutowiringInspection") @Autowired private lateinit var myService: MyServiceImpl @Test fun `test foo`() = runTest { runBlocking { myService.foo(cancelRequest { id = 1 message = "Canceled" }) } } }
MyServiceImpl
@GrpcService class MyServiceImpl @Autowired constructor( private val myTopicService: TopicV2Service<CustomMessage>, ) : MyServiceGrpcKt.MyServiceCoroutineImplBase() { override suspend fun foo(request: CancelRequest): Empty { .... runBlocking { myTopicService.publish(customMessage()) } } }
TopicV2Service
open class TopicV2Service<M : Message>(private val topic: TopicV2<M>) { @Autowired private lateinit var queuesProperties: QueuesProperties private val client = SnsAsyncClient.create() private val protobufPrinter = JsonFormat.printer() private val topicArn by lazy { client.createTopic { it.name("${queuesProperties.snsPrefix}${topic.name}") } .join() .topicArn() } suspend fun publish(message: M) { val wrappedMessage = WrappedMessage(topic.name, protobufPrinter.print(message)).let { Json.encodeToString(it) } client.publish { it.topicArn(topicArn).message(wrappedMessage) }.await() } }
错误信息
java.lang.NullPointerException: Cannot invoke "miniproj.toy1.queues.TopicV2.getName()" because "this.topic" is null at miniproj.toy1.queues.TopicV2Service.publish(TopicV2Service.kt:29) at miniproj.toy1.inv_service.services.MyServiceImpl$foo$3$1$1.invokeSuspend(MyServiceImpl.kt:485)
问题原因及解决办法
核心原因
TopicV2Service的publish是协程suspend函数,Mockito默认不支持拦截suspend方法的调用,导致MockBean没有生效,最终执行了真实方法。而Mock对象的构造参数topic为null,直接触发空指针异常。
解决步骤
添加协程Mock依赖
在测试依赖中加入mockito-kotlin和mockito-inline,支持协程方法的Mock:// Gradle示例 testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0" testImplementation "org.mockito:mockito-inline:4.8.1"明确Mock suspend方法的行为
在测试方法中用coEvery指定suspend fun publish的返回逻辑,避免执行真实方法:@Test fun `test foo`() = runTest { // 指定publish方法什么都不做 coEvery { myTopicService.publish(any()) } returns Unit myService.foo(cancelRequest { id = 1 message = "Canceled" }) // 可选:验证publish方法是否被正确调用 coVerify(myTopicService).publish(any()) }优化测试协程写法
不要嵌套runBlocking和runTest,runTest本身就是为测试优化的协程作用域,直接使用即可(如上面代码所示)。检查上下文Bean优先级
确保@DataJpaTest切片测试中,真实的TopicV2Service没有被自动扫描到Spring上下文。如果TestApplication包含了该类的配置,可能导致Spring优先注入真实Bean而非MockBean,此时可以调整@ContextConfiguration的范围,或通过@MockBean(name = "...")明确指定要替换的Bean。
内容的提问来源于stack exchange,提问作者dann.kim

