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) { + } + } + }