I have a regression upgrading to Spring Boot 2.3.0.RELEASE.

According to https://github.com/spring-projects/spring-boot/issues/7033

I try creating a Mock manually like

@Primary
@Bean
MyRepository testBean(MyRepository real){
        var mock = mock(MyRepository.class,
        AdditionalAnswers.delegatesTo(real));
    when(mock.count()).thenReturn(100L);
    return mock;
}

Calling verify() on that mock fails with Argument passed to verify() is of type $Proxy82 and is not a mock!

Comment From: eiswind

Some more investigations showed that it seems to correlate with the BootstrapMode. It appears that is has been changed to default to DEFERRED. When I switch to DEFAULT everything works as expected.

Are there any ideas how to deal with the deferred repositories?

Comment From: wilkinsona

Thanks for opening an issue and for the additional analysis, @eiswind. The deferred bootstrap mode results in the repository being proxied. Mockito needs to be passed the proxy's target rather than the proxy. You can get the underlying target using AopProxyUtils:

Object target = AopProxyUtils.getSingletonTarget(real);
MyRepository mock = Mockito.mock(MyRepository.class, AdditionalAnswers.delegatesTo(target));

Please let us know if this resolves your problem.

Comment From: eiswind

Unfortunately this does not solve the problem. My guess was be that there gets a proxy created for the configured mock bean. But un-proxing the wired instance does not work either, results in null. So no idea. I could switch to default mode for the test, but then I didn't really understand it :)

Comment From: wilkinsona

Interesting. That worked for me in an example I created, however it did behave slightly differently to my understanding of the behaviour that you've described. You may need to extract the target before using it in your test rather than when defining the bean. If that doesn't help, can you please share a minimal sample that reproduces the problem so that we can be sure that we're investigating the exact problem that you're seeing?

Comment From: eiswind

Thanks @wilkinsona .

@SpringBootTest
class FailingJpaTest {

    @TestConfiguration
    static class TestConfig{
        @Primary
        @Bean
        MyRepository testBean(MyRepository real){
            Object target = AopProxyUtils.getSingletonTarget(real);
            var mock = mock(MyRepository.class,
                    AdditionalAnswers.delegatesTo(target));

            return mock;
        }
    }

    @Autowired
    MyRepository repository;

    @Test
    void verifyFails() {
        verify(repository, times(0)).count();
    }

}

This test fails at my side. I tried

@Test
void verifyFails() {
    verify((MyRepository)AopProxyUtils.getSingletonTarget(repository), times(0)).count();
}

But that gives org.mockito.exceptions.misusing.NullInsteadOfMockException

Comment From: wilkinsona

That's a bit too minimal, unfortunately. When I try to replicate it a get a bean currently in creation exception from testBean. Can you please create and share a minimal yet complete project that reproduces the behaviour you're seeing?

Comment From: odrotbohm

I just spoke to @jhoeller and a workaround should be to use ObjectProvider at the injection point as that avoids the indirection via a LazyInitTargetSource. Not pretty but something to start with. Just as Andy, I failed to get the example past the BeanCurrentlyInCreationException.

Comment From: eiswind

@odrotbohm @jhoeller using ObjectProvider did the trick!

Could make it work in a single file, can't believe spring recognizes the repository.

@SpringBootTest
class JpaTest {

    @Entity
    public static class MyEntity{
        @Id
        Long id;
    }


    @TestConfiguration
    static class TestConfig{
        @Primary
        @Bean
        MyRepository testBean(MyRepository real){
            var mock = mock(MyRepository.class,
                    AdditionalAnswers.delegatesTo(real));

            return mock;
        }
    }

    @Autowired
    ObjectProvider<MyRepository> repository;

    @Test
    void verify() {
        verify(repository.getIfAvailable(), times(0)).count();
    }

}

interface MyRepository extends JpaRepository<JpaTest.MyEntity,Long>{}

Comment From: odrotbohm

Okay, that's great to hear. Let's take that back to the team and see how we can make this less painful OOTB. Thanks for your patience!

Comment From: eiswind

sorry for the incomplete example. the last one should work. https://github.com/spring-projects/spring-boot/issues/21488#issuecomment-630910355

Comment From: wilkinsona

Thanks, @eiswind. Now that the workaround is working again, I think we're back to needing to fix #7033 to improve the situation here. I'm going to close this one and hopefully use it as motivation to take another look at getting @SpyBean working with Data JPA repositories.

Comment From: NicklasWallgren

I tried the ObjectProvider trick above, and it worked great, but I have encountered another issue.

It seems like I'm unable to mock CrudRepository::save.

The following mock results in an exception. Mockito.when(repository.getIfAvailable().save(any(MyEntity.class))).then(returnsFirstArg());.

Caused by: java.lang.IllegalArgumentException: Target object must not be null
    at org.springframework.util.Assert.notNull(Assert.java:198)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setWrappedInstance(AbstractNestablePropertyAccessor.java:195)
    at org.springframework.beans.BeanWrapperImpl.setWrappedInstance(BeanWrapperImpl.java:153)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setWrappedInstance(AbstractNestablePropertyAccessor.java:183)
    at org.springframework.beans.AbstractNestablePropertyAccessor.<init>(AbstractNestablePropertyAccessor.java:122)
    at org.springframework.beans.BeanWrapperImpl.<init>(BeanWrapperImpl.java:103)
    at org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper.<init>(DirectFieldAccessFallbackBeanWrapper.java:36)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.getId(JpaMetamodelEntityInformation.java:159)
    at org.springframework.data.repository.core.support.AbstractEntityInformation.isNew(AbstractEntityInformation.java:42)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.isNew(JpaMetamodelEntityInformation.java:246)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:553)
    at jdk.internal.reflect.GeneratedMethodAccessor174.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.data.repository.core.support.ImplementationInvocationMetadata.invoke(ImplementationInvocationMetadata.java:72)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:382)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:205)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:549)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)

Comment From: wilkinsona

@NicklasWallgren Thanks for letting us know. It's hard to tell what the cause is from the stacktrace above, but on the face of it, I don't think it's the same problem as this issue was tracking. It could be another variant of #7033 but I'm not sure that it is. I think the best option at this point is a new issue for now at least. If you'd like us to spend some more time investigating, can you please open one and provide a minimal sample that reproduces the stacktrace you have shared above?

Comment From: odrotbohm

Just a quick note: the stack trace suggests that the mock is not used as it shows SimpleJpaRepository doing its thing trying to persist a null handed to ….save(…).

Comment From: NicklasWallgren

Thanks for the response. I have created a minimal sample which can be found here. https://github.com/NicklasWallgren/springboot-issue-21488/blob/master/src/test/java/com/example/demo/DemoApplicationTests.java

I'll create a new issue if you find it appropriate.

Comment From: wilkinsona

Thanks, @NicklasWallgren.

@odrotbohm was correct above and it's happening because you are not configuring the exceptions in the way that is required when AdditionalAnswers.delegatesTo(real) is used. Taken from its javadoc:

This feature suffers from the same drawback as the spy. The mock will call the delegate if you use regular when().then() stubbing style. Since the real implementation is called this might have some side effects. Therefore you should to use the doReturn|Throw|Answer|CallRealMethod stubbing style. Example:

``` List listWithDelegate = mock(List.class, AdditionalAnswers.delegatesTo(awesomeList));

//Impossible: real method is called so listWithDelegate.get(0) throws IndexOutOfBoundsException (the list is yet empty) when(listWithDelegate.get(0)).thenReturn("foo");

//You have to use doReturn() for stubbing doReturn("foo").when(listWithDelegate).get(0); ```

This means that your saveUsingSpy() method should be modified to look like this:

@Test
void saveUsingSpy() {
    doAnswer(returnsFirstArg()).when(repository.getIfAvailable()).save(any(MyEntity.class));

    final MyEntity myEntity = new MyEntity(1L);
    final MyEntity persistedEntity = repository.getIfAvailable().save(myEntity);

    assertEquals(myEntity, persistedEntity);
}

With this change in place, all 4 tests in your sample pass for me.

Comment From: NicklasWallgren

@wilkinsona Thanks for the thorough explanation.

What I'm actually trying to achieve by using "custom" mock/spy bean factories, is to be able to re-use the active ApplicationContext, unlike @MockBean or @SpyBean which forces the context to be re-created.

So my thought is to create spy bean factories (like MyRepository testBean) for every bean that currently has the @MockBean annotation, and then use @Autowire instead. And then reset the mocks after each integration test.

Would you say this is a sane approach, instead of using MockBean and/or SpyBean?

Comment From: wilkinsona

@MockBean and @SpyBean only force the context to be recreated if you do not have a consistent set of mocks and spies across all of your tests. If you have the same @MockBean and @SpyBean configuration in every test class, the tests will all share a single application context.

If you have any further questions, please follow up on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.