Stubbing method calls on a @MockitoSpyBean in conjunction with ScopedProxyMode.TARGET_CLASS results in java.lang.IllegalStateException: Failed to unwrap proxied object.
Steps to reproduce:
- Write a Spring bean that uses
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS). - In an integration test, make it a spy bean.
- Mock a call to the spy bean.
- Run the test.
I think the issue could be in org.springframework.test.context.bean.override.mockito.SpringMockResolver.getUltimateTargetObject. It invokes the method org.springframework.aop.framework.Advised#getTargetSource() on the spy bean and is (indirectly) called by org.mockito.internal.stubbing.StubberImpl#when(T mock), which means when(T mock) is not finished when getTargetSource() is called.
Please find the attached reproducer which has a full setup. Slightly more detailed information can be found in the contained README.md.
Comment From: sbrannen
Hi @i-am-not-giving-my-name-to-a-machine,
Congratulations on submitting your first issue for the Spring Framework! 👍
I have to admit: I don't think I've ever seen anyone use @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) for self injection.
I suppose that works in general, but as you've noticed it does not work with @MockitoSpyBean. The reason is that ScopedProxyFactoryBean is used to create the scoped proxy for MyService, which uses a SimpleBeanTargetSource, which is not a static TargetSource. Consequently, SpringMockResolver.getUltimateTargetObject(Object) is not able to unwrap the proxy, and that's why you see that exception.
I'll repurpose this ticket to see if I can improve the exception message, so that it's clear that a scoped proxy cannot be spied on.
Aside from that, I recommend you read our documentation on circular dependencies and self injection.
For your particular scenario, I've verified that your test passes if you remove @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) and add @Lazy to the MyService constructor argument.
@Service
public class MyService {
private final MyService self;
public MyService(@Lazy MyService self) {
this.self = self;
}
public String doSomething() {
return self.doSomething0();
}
@Cacheable(cacheNames = "my-cache")
public String doSomething0() {
return "Foo";
}
}
Let me know if that works for you.
Cheers
Comment From: i-am-not-giving-my-name-to-a-machine
Hi @sbrannen,
thank you for the fast response.
Yes, I can confirm that switching to @Lazy and removing @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) does indeed work. I also added @EnableCaching to the class MySpringBootTest and verified that the @Cacheable annotation works as expected: It does. 👍
As you already guessed, I only used @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) to inject a self-reference, as I wanted to use Spring's caching features from within the same class. So thank you for taking the time to investigate and for showing me a better solution.
Fwiw, I think that it should be possible to spy on scoped proxies. But I also understand that it is probably an edge-case or somebody else would've reported an issue in the past already.
Again, thank you for the help.
Best regards
Comment From: sbrannen
Hi @i-am-not-giving-my-name-to-a-machine,
Thanks for trying out @Lazy and letting us know it works for your purposes.
As a side note, in commit c0c94d5d86d741d407e35a972c2313157d3d3362 you can see that your use case would now fail with a stack trace similar to the following, which hopefully makes it clearer to users.
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'myService': Post-processing of
FactoryBean's singleton object failed
...
Caused by: java.lang.IllegalStateException:
@MockitoSpyBean cannot be applied to bean 'myService', because
it is a Spring AOP proxy with a non-static TargetSource. Perhaps you
have attempted to spy on a scoped proxy, which is not supported.
at ...MockitoSpyBeanOverrideHandler.createSpy(MockitoSpyBeanOverrideHandler.java:78)
Fwiw, I think that it should be possible to spy on scoped proxies. But I also understand that it is probably an edge-case or somebody else would've reported an issue in the past already.
I certainly understand the desire for that to work!
However, it has actually come up several times over the years, and we have unfortunately never figured out a way to make it work reliably.
-
29215
- https://github.com/spring-projects/spring-boot/issues/32632
- https://github.com/spring-projects/spring-boot/issues/19020
- Spring Boot commit https://github.com/spring-projects/spring-boot/commit/52050c173c35e8b96722051031ab55774f524967
- https://github.com/spring-projects/spring-boot/issues/17817
- ...