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:

  1. Write a Spring bean that uses @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS).
  2. In an integration test, make it a spy bean.
  3. Mock a call to the spy bean.
  4. 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.

spy-beans.zip

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
  • ...