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.