Search code examples
spring-bootmockitospring-boot-testspyspring-data-jdbc

Stubbing a @SpyBean in Spring Boot test is failing with IllegalArgumentException: Aggregate instance must not be null


I am having an ordinary spring boot integration test, which works fine with @Autowired and @MockBean, having all real, just the rest templates to the outside world are mocked

@SpringBootTest
@DirtiesContext
@ExtendWith(ConsoleReporterExtension.class)
@ActiveProfiles("it")
@SpringJUnitConfig(Application.class)
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
@ContextConfiguration(initializers = {TestcontainersInitializer.class}) // for some docker DB
...
@Autowired
private SomeRepository someRepository;

then I needed to simulate an issue during persistence and decided to spy on the bean:

//@Autowired
@SpyBean
private SomeRepository someRepository;

Rest of the tests are still working fine, so the spied bean is properly propagated inside the app and because it is not stubbed it just behaves same like as it was auto-wired. But when I stub it to throw an one-time exception during save

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

it fails on that stubbing, as if someone is passing null to the save method, but it is not the case, especially that we are not there yet, rather it happens during the stubbing definition. What am I missing? (spring boot v2.7.10)

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>)

Btw the repo is just a Crud one (even tried a hack to explicitly override the save method, but this obviously did not change anything)

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

//<S extends SomeEntity> S save(S entity);

Solution

  • it fails on that stubbing, as if someone is passing null to the save method, but it is not the case, especially that we are not there yet, rather it happens during the stubbing definition.

    In fact, it is the case. You are invoking the method and passing null right here in this line:

    when(someRepository.save(any()))
    

    For reference, here's the implementation of ArgumentMatchers#any:

    public static <T> T any() {
        reportMatcher(Any.ANY);
        return null; // <- returns null
    }
    

    Java is eagerly evaluated and always invokes methods before passing their return value to another method. when(obj.method()) must invoke method() before when can be called. Mockito cannot change this fact.

    If you want to avoid calling the method, use the doThrow/doCallRealMethod forms:

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

    More info: