You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用@MockBean未调用thenCallRealMethod却执行真实方法问题排查

JPA单元测试中@MockBean未生效,Mock对象执行真实方法导致NPE

在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,直接触发空指针异常。

解决步骤

  1. 添加协程Mock依赖
    在测试依赖中加入mockito-kotlin和mockito-inline,支持协程方法的Mock:

    // Gradle示例
    testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0"
    testImplementation "org.mockito:mockito-inline:4.8.1"
    
  2. 明确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())
    }
    
  3. 优化测试协程写法
    不要嵌套runBlocking和runTest,runTest本身就是为测试优化的协程作用域,直接使用即可(如上面代码所示)。

  4. 检查上下文Bean优先级
    确保@DataJpaTest切片测试中,真实的TopicV2Service没有被自动扫描到Spring上下文。如果TestApplication包含了该类的配置,可能导致Spring优先注入真实Bean而非MockBean,此时可以调整@ContextConfiguration的范围,或通过@MockBean(name = "...")明确指定要替换的Bean。


内容的提问来源于stack exchange,提问作者dann.kim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:39:51