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

Spring Boot测试@SpyBean存根失败:Aggregate instance must not be null

问题描述

我正在编写Spring Boot集成测试(版本2.7.10),原本用@Autowired注入SomeRepository,配合@MockBean模拟外部RestTemplate时一切正常,测试类配置如下:

@SpringBootTest
@DirtiesContext
@ExtendWith(ConsoleReporterExtension.class)
@ActiveProfiles("it")
@SpringJUnitConfig(Application.class)
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
@ContextConfiguration(initializers = {TestcontainersInitializer.class}) // 用于Docker数据库
// ...
@Autowired
private SomeRepository someRepository;

后来需要模拟持久化时的异常,改用@SpyBean监听该Repository:

//@Autowired
@SpyBean
private SomeRepository someRepository;

未配置存根时测试正常,说明SpyBean已正确生效。但当我尝试存根save方法,让它首次调用抛出异常、后续调用执行真实方法时:

when(someRepository.save(any()))
            .thenThrow(new OptimisticLockingFailureException("some concurrency issue"))
            .thenCallRealMethod();

在存根定义阶段就抛出IllegalArgumentException,提示"Aggregate instance must not be null!",但此时并未实际调用save方法传入null值。SomeRepository是继承自CrudRepository的普通接口:

@Repository
public interface SomeRepository extends CrudRepository<SomeEntity, Long> {

// <S extends SomeEntity> S save(S entity); 曾尝试显式重写,无效果
}
报错堆栈
java.lang.IllegalArgumentException: Aggregate instance must not be null!

at org.springframework.util.Assert.notNull(Assert.java:201)
at org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:153)
at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:78)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:530)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:286)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:640)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:139)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:81)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at jdk.proxy2/jdk.proxy2.$Proxy223.save(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.mockito.internal.util.reflection.ReflectionMemberAccessor.invoke(ReflectionMemberAccessor.java:48)
at org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations.answer(ForwardsInvocations.java:49)
at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:110)
at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:29)
at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:34)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:82)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:56)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor$DispatcherDefaultingToRealMethod.interceptAbstract(MockMethodInterceptor.java:161)
at SomeRepository$MockitoMock$cs2Dymza.save(Unknown Source)
at SomeRepository$MockitoMock$cs2Dymza$$FastClassBySpringCGLIB$$b0e4e380.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
at SomeRepository$MockitoMock$cs2Dymza$$EnhancerBySpringCGLIB$$6546aaba.save(<generated>)
解决方案

问题原因

使用when(someRepository.save(any())).thenThrow(...).thenCallRealMethod()这种链式语法时,Mockito会提前触发一次真实的save方法调用——因为when()语法需要先执行目标方法来获取返回值,用于后续的存根逻辑。而此时传入的参数是any()匹配器对应的null值,导致Spring Data JDBC内部的JdbcAggregateTemplate.save()方法触发Assert.notNull()断言失败。

解决办法

改用Mockito的doThrow().doCallRealMethod()语法,这种方式不会提前触发真实方法调用,只是直接配置方法的行为:

doThrow(new OptimisticLockingFailureException("some concurrency issue"))
    .doCallRealMethod()
    .when(someRepository)
    .save(any());

补充说明

Spring Data的Repository是通过动态代理生成的Bean,和普通Java类的Spy行为存在差异。when()语法依赖方法调用的返回值,对于Repository代理来说会触发完整的方法执行链路,而do...系列方法是直接Stub行为,绕过了提前调用真实方法的逻辑,因此可以避免传入null导致的断言错误。

内容的提问来源于stack exchange,提问作者radio

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 09:17:34