From c0c94d5d86d741d407e35a972c2313157d3d3362 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:05:15 +0100 Subject: [PATCH] =?UTF-8?q?Reject=20attempt=20to=20use=20@=E2=81=A0Mockito?= =?UTF-8?q?SpyBean=20with=20a=20scoped=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, an attempt to use @MockitoSpyBean to spy on a scoped proxy configured with @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) resulted in an exception thrown by Mockito when the spy was stubbed. The exception message stated "Failed to unwrap proxied object" but did not provide any further insight or context for the user. The reason is that ScopedProxyFactoryBean is used to create such a scoped proxy, which uses a SimpleBeanTargetSource, which is not a static TargetSource. Consequently, SpringMockResolver.getUltimateTargetObject(Object) is not able to unwrap the proxy. In order to improve diagnostics for users, this commit eagerly detects an attempt to spy on a scoped proxy and throws an exception with a meaningful message. The following is an example, trimmed stack trace from the test suite. org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'myScopedProxy': Post-processing of FactoryBean's singleton object failed ... Caused by: java.lang.IllegalStateException: @MockitoSpyBean cannot be applied to bean 'myScopedProxy', 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) Closes gh-35722 --- .../MockitoSpyBeanOverrideHandler.java | 2 + .../override/mockito/SpringMockResolver.java | 21 ++++++ ...MockitoSpyBeanConfigurationErrorTests.java | 67 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java index c344d0e2c38..e9913ab44e0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java @@ -69,9 +69,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { } private Object createSpy(String name, Object instance) { + SpringMockResolver.rejectUnsupportedSpyTarget(name, instance); Class> resolvedTypeToOverride = getBeanType().resolve(); Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override"); Assert.isInstanceOf(resolvedTypeToOverride, instance); + if (Mockito.mockingDetails(instance).isSpy()) { return instance; } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpringMockResolver.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpringMockResolver.java index 12e31cd0885..f6f71b5082f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpringMockResolver.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpringMockResolver.java @@ -79,4 +79,25 @@ public class SpringMockResolver implements MockResolver { return candidate; } + /** + * Reject the supplied bean if it is not a supported candidate to spy on. + *
Specifically, this method ensures that the bean is not a Spring AOP proxy + * with a non-static {@link TargetSource}. + * @param beanName the name of the bean to spy on + * @param bean the bean to spy on + * @since 7.0 + * @see #getUltimateTargetObject(Object) + */ + static void rejectUnsupportedSpyTarget(String beanName, Object bean) throws IllegalStateException { + if (SPRING_AOP_PRESENT) { + if (AopUtils.isAopProxy(bean) && bean instanceof Advised advised && + !advised.getTargetSource().isStatic()) { + throw new IllegalStateException(""" + @MockitoSpyBean cannot be applied to bean '%s', 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.""".formatted(beanName)); + } + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java index b1d382e1729..38ee10d9615 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java @@ -20,15 +20,22 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Component; import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerTestUtils; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link MockitoSpyBean @MockitoSpyBean}. * * @author Stephane Nicoll + * @author Sam Brannen */ class MockitoSpyBeanConfigurationErrorTests { @@ -73,6 +80,39 @@ class MockitoSpyBeanConfigurationErrorTests { List.of("bean1", "bean2")); } + @Test // gh-35722 + void mockitoSpyBeanCannotSpyOnScopedProxy() { + var context = new AnnotationConfigApplicationContext(); + context.register(MyScopedProxy.class); + BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ScopedProxyTestCase.class, context); + context.refresh(); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> context.getBean(MyScopedProxy.class)) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage(""" + @MockitoSpyBean cannot be applied to bean 'myScopedProxy', 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."""); + } + + @Test // gh-35722 + void mockitoSpyBeanCannotSpyOnSelfInjectionScopedProxy() { + var context = new AnnotationConfigApplicationContext(); + context.register(MySelfInjectionScopedProxy.class); + BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(SelfInjectionScopedProxyTestCase.class, context); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage(""" + @MockitoSpyBean cannot be applied to bean 'mySelfInjectionScopedProxy', 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."""); + } + static class ByTypeSingleLookup { @@ -88,4 +128,31 @@ class MockitoSpyBeanConfigurationErrorTests { } + static class ScopedProxyTestCase { + + @MockitoSpyBean + MyScopedProxy myScopedProxy; + + } + + static class SelfInjectionScopedProxyTestCase { + + @MockitoSpyBean + MySelfInjectionScopedProxy mySelfInjectionScopedProxy; + + } + + @Component("myScopedProxy") + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class MyScopedProxy { + } + + @Component("mySelfInjectionScopedProxy") + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class MySelfInjectionScopedProxy { + + MySelfInjectionScopedProxy(MySelfInjectionScopedProxy self) { + } + } + }