diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java index a25dff0a1d..8aaceb8529 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * Mark a composed annotation as eligible for Bean Override processing. * @@ -37,11 +39,13 @@ import java.lang.annotation.Target; * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @Documented +@Reflective(BeanOverrideReflectiveProcessor.class) public @interface BeanOverride { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 466a827c09..c8116f6278 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.aot.AbstractAotProcessor; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; @@ -105,6 +106,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, private void replaceDefinition(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + // NOTE: This method supports 3 distinct scenarios which must be accounted for. + // + // 1) JVM runtime + // 2) AOT processing + // 3) AOT runtime + if (!(beanFactory instanceof BeanDefinitionRegistry registry)) { throw new IllegalStateException("Cannot process bean override with a BeanFactory " + "that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass().getName()); @@ -147,12 +154,24 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, if (existingBeanDefinition != null) { // Validate the existing bean definition. + // + // Applies during "JVM runtime", "AOT processing", and "AOT runtime". validateBeanDefinition(beanFactory, beanName); } + else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) { + // There was no existing bean definition, but during "AOT processing" we + // do not register the "pseudo" bean definition since our AOT support + // cannot automatically convert that to a functional bean definition for + // use at "AOT runtime". Furthermore, by not registering a bean definition + // for a nonexistent bean, we allow the "JVM runtime" and "AOT runtime" + // to operate the same in the following else-block. + } else { // There was no existing bean definition, so we register the "pseudo" bean // definition to ensure that a suitable bean definition exists for the given // bean name for proper autowiring candidate resolution. + // + // Applies during "JVM runtime" and "AOT runtime". registry.registerBeanDefinition(beanName, pseudoBeanDefinition); } @@ -163,6 +182,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, // Now we have an instance (the override) that we can register. At this stage, we don't // expect a singleton instance to be present. If for some reason a singleton instance // already exists, the following will throw an exception. + // + // As a bonus, by manually registering a singleton during "AOT processing", we allow + // GenericApplicationContext's preDetermineBeanType() method to transparently register + // runtime hints for a proxy generated by the above createOverride() invocation -- + // for example, when @MockitoBean creates a mock based on a JDK dynamic proxy. beanFactory.registerSingleton(beanName, override); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java index 07df3d4f08..3753f348ea 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java @@ -17,13 +17,8 @@ package org.springframework.test.context.bean.override; import java.util.Set; -import java.util.function.Consumer; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; @@ -34,6 +29,7 @@ import org.springframework.test.context.MergedContextConfiguration; * * @author Simon Baslé * @author Stephane Nicoll + * @author Sam Brannen * @since 6.2 */ class BeanOverrideContextCustomizer implements ContextCustomizer { @@ -56,43 +52,25 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - if (!(context instanceof BeanDefinitionRegistry registry)) { - throw new IllegalStateException("Cannot process bean overrides with an ApplicationContext " + - "that doesn't implement BeanDefinitionRegistry: " + context.getClass().getName()); - } - registerInfrastructure(registry); + ConfigurableBeanFactory beanFactory = context.getBeanFactory(); + // Since all three Bean Override infrastructure beans are never injected as + // dependencies into other beans within the ApplicationContext, it is sufficient + // to register them as manual singleton instances. In addition, registration of + // the BeanOverrideBeanFactoryPostProcessor as a singleton is a requirement for + // AOT processing, since a bean definition cannot be generated for the + // Set argument that it accepts in its constructor. + BeanOverrideRegistrar beanOverrideRegistrar = new BeanOverrideRegistrar(beanFactory); + beanFactory.registerSingleton(REGISTRAR_BEAN_NAME, beanOverrideRegistrar); + beanFactory.registerSingleton(INFRASTRUCTURE_BEAN_NAME, + new BeanOverrideBeanFactoryPostProcessor(this.metadata, beanOverrideRegistrar)); + beanFactory.registerSingleton(EARLY_INFRASTRUCTURE_BEAN_NAME, + new WrapEarlyBeanPostProcessor(beanOverrideRegistrar)); } Set getMetadata() { return this.metadata; } - private void registerInfrastructure(BeanDefinitionRegistry registry) { - addInfrastructureBeanDefinition(registry, BeanOverrideRegistrar.class, REGISTRAR_BEAN_NAME, - constructorArgs -> {}); - - RuntimeBeanReference registrarReference = new RuntimeBeanReference(REGISTRAR_BEAN_NAME); - addInfrastructureBeanDefinition(registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, - constructorArgs -> constructorArgs.addIndexedArgumentValue(0, registrarReference)); - addInfrastructureBeanDefinition(registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, - constructorArgs -> { - constructorArgs.addIndexedArgumentValue(0, this.metadata); - constructorArgs.addIndexedArgumentValue(1, registrarReference); - }); - } - - private void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry, - Class clazz, String beanName, Consumer constructorArgumentsConsumer) { - - if (!registry.containsBeanDefinition(beanName)) { - RootBeanDefinition definition = new RootBeanDefinition(clazz); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues(); - constructorArgumentsConsumer.accept(constructorArguments); - registry.registerBeanDefinition(beanName, definition); - } - } - @Override public boolean equals(Object other) { if (other == this) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java new file mode 100644 index 0000000000..787fcd0ff3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; + +import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; + +/** + * {@link ReflectiveProcessor} that processes {@link BeanOverride @BeanOverride} + * annotations. + * + * @author Sam Brannen + * @since 6.2 + */ +class BeanOverrideReflectiveProcessor implements ReflectiveProcessor { + + @Override + public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { + MergedAnnotations.from(element) + .get(BeanOverride.class) + .synthesize(MergedAnnotation::isPresent) + .map(BeanOverride::value) + .ifPresent(clazz -> hints.registerType(clazz, INVOKE_DECLARED_CONSTRUCTORS)); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java index 3d49207ef0..d9f749a4b4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java @@ -22,10 +22,7 @@ import java.util.Map; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -36,36 +33,31 @@ import org.springframework.util.StringUtils; * for test execution listeners. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ -class BeanOverrideRegistrar implements BeanFactoryAware { +class BeanOverrideRegistrar { private final Map beanNameRegistry = new HashMap<>(); private final Map earlyOverrideMetadata = new HashMap<>(); - @Nullable - private ConfigurableBeanFactory beanFactory; + private final ConfigurableBeanFactory beanFactory; - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) { - throw new IllegalStateException("Cannot process bean override with a BeanFactory " + - "that doesn't implement ConfigurableBeanFactory: " + beanFactory.getClass().getName()); - } - this.beanFactory = cbf; + BeanOverrideRegistrar(ConfigurableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null"); + this.beanFactory = beanFactory; } /** - * Check {@link #markWrapEarly(OverrideMetadata, String) early override} + * Check {@linkplain #markWrapEarly(OverrideMetadata, String) early override} * records and use the {@link OverrideMetadata} to create an override - * instance from the provided bean, if relevant. + * instance based on the provided bean, if relevant. */ Object wrapIfNecessary(Object bean, String beanName) throws BeansException { OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) { - Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null"); bean = metadata.createOverride(beanName, null, bean); metadata.track(bean, this.beanFactory); } @@ -99,7 +91,6 @@ class BeanOverrideRegistrar implements BeanFactoryAware { try { ReflectionUtils.makeAccessible(field); Object existingValue = ReflectionUtils.getField(field, target); - Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null"); Object bean = this.beanFactory.getBean(beanName, field.getType()); if (existingValue == bean) { return; diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java new file mode 100644 index 0000000000..ed102c702d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + +import org.springframework.util.ReflectionUtils; + +/** + * Utility class that detects {@code org.mockito} annotations as well as the + * annotations in this package (like {@link MockitoBeanSettings @MockitoBeanSettings}). + * + * @author Simon Baslé + * @author Sam Brannen + */ +abstract class MockitoAnnotationDetector { + + private static final String MOCKITO_BEAN_PACKAGE = MockitoBeanSettings.class.getPackageName(); + + private static final String ORG_MOCKITO_PACKAGE = "org.mockito"; + + private static final Predicate isMockitoAnnotation = annotation -> { + String packageName = annotation.annotationType().getPackageName(); + return (packageName.startsWith(MOCKITO_BEAN_PACKAGE) || + packageName.startsWith(ORG_MOCKITO_PACKAGE)); + }; + + static boolean hasMockitoAnnotations(Class testClass) { + if (isAnnotated(testClass)) { + return true; + } + // TODO Ideally we should short-circuit the search once we've found a Mockito annotation, + // since there's no need to continue searching additional fields or further up the class + // hierarchy; however, that is not possible with ReflectionUtils#doWithFields. Plus, the + // previous invocation of isAnnotated(testClass) only finds annotations declared directly + // on the test class. So, we'll likely need a completely different approach that combines + // the "test class/interface is annotated?" and "field is annotated?" checks in a single + // search algorithm. + AtomicBoolean found = new AtomicBoolean(); + ReflectionUtils.doWithFields(testClass, field -> found.set(true), MockitoAnnotationDetector::isAnnotated); + return found.get(); + } + + private static boolean isAnnotated(AnnotatedElement annotatedElement) { + return Arrays.stream(annotatedElement.getAnnotations()).anyMatch(isMockitoAnnotation); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 46cf654c18..2785fd7ab1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -29,7 +29,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; @@ -58,14 +57,16 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList @Override public void beforeTestMethod(TestContext testContext) throws Exception { - if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + Class testClass = testContext.getTestClass(); + if (MockitoTestExecutionListener.mockitoPresent && MockitoAnnotationDetector.hasMockitoAnnotations(testClass)) { resetMocks(testContext.getApplicationContext(), MockReset.BEFORE); } } @Override public void afterTestMethod(TestContext testContext) throws Exception { - if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + Class testClass = testContext.getTestClass(); + if (MockitoTestExecutionListener.mockitoPresent && MockitoAnnotationDetector.hasMockitoAnnotations(testClass)) { resetMocks(testContext.getApplicationContext(), MockReset.AFTER); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java index 46d7799889..0630e355cd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java @@ -16,12 +16,6 @@ package org.springframework.test.context.bean.override.mockito; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -31,7 +25,6 @@ import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; /** * {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} @@ -127,42 +120,4 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener } } - - /** - * Utility class that detects {@code org.mockito} annotations as well as the - * annotations in this package (like {@link MockitoBeanSettings @MockitoBeanSettings}). - */ - private static class MockitoAnnotationDetector { - - private static final String MOCKITO_BEAN_PACKAGE = MockitoBeanSettings.class.getPackageName(); - - private static final String ORG_MOCKITO_PACKAGE = "org.mockito"; - - private static final Predicate isMockitoAnnotation = annotation -> { - String packageName = annotation.annotationType().getPackageName(); - return (packageName.startsWith(MOCKITO_BEAN_PACKAGE) || - packageName.startsWith(ORG_MOCKITO_PACKAGE)); - }; - - static boolean hasMockitoAnnotations(Class testClass) { - if (isAnnotated(testClass)) { - return true; - } - // TODO Ideally we should short-circuit the search once we've found a Mockito annotation, - // since there's no need to continue searching additional fields or further up the class - // hierarchy; however, that is not possible with ReflectionUtils#doWithFields. Plus, the - // previous invocation of isAnnotated(testClass) only finds annotations declared directly - // on the test class. So, we'll likely need a completely different approach that combines - // the "test class/interface is annotated?" and "field is annotated?" checks in a single - // search algorithm. - AtomicBoolean found = new AtomicBoolean(); - ReflectionUtils.doWithFields(testClass, field -> found.set(true), MockitoAnnotationDetector::isAnnotated); - return found.get(); - } - - private static boolean isAnnotated(AnnotatedElement annotatedElement) { - return Arrays.stream(annotatedElement.getAnnotations()).anyMatch(isMockitoAnnotation); - } - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index 7695515c8d..e5fa0317f8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java @@ -155,7 +155,6 @@ class AotIntegrationTests extends AbstractAotTests { runEndToEndTests(testClasses, false); } - @Disabled("Comment out to run Bean Override integration tests in AOT mode") @Test void endToEndTestsForBeanOverrides() { List> testClasses = createTestClassScanner() diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java index 4ab72df7e6..4d791caeee 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.test.context.aot; import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -24,7 +25,10 @@ import java.util.stream.Stream; import javax.sql.DataSource; +import org.assertj.core.util.Arrays; +import org.easymock.EasyMockSupport; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.aot.AotDetector; import org.springframework.aot.generate.DefaultGenerationContext; @@ -37,6 +41,7 @@ import org.springframework.aot.test.generate.CompilerFiles; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.aot.AbstractAotProcessor; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.TestCompiler; import org.springframework.javapoet.ClassName; @@ -47,6 +52,9 @@ import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterShar import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests; import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests; import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests; +import org.springframework.test.context.aot.samples.bean.override.EasyMockBeanJupiterTests; +import org.springframework.test.context.aot.samples.bean.override.MockitoBeanJupiterTests; +import org.springframework.test.context.aot.samples.common.GreetingService; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.aot.samples.jdbc.SqlScriptsSpringJupiterTests; import org.springframework.test.context.aot.samples.web.WebSpringJupiterTests; @@ -67,6 +75,7 @@ import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTR import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS; import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS; import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.proxies; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -97,6 +106,8 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { BasicSpringJupiterTests.NestedTests.class, BasicSpringTestNGTests.class, BasicSpringVintageTests.class, + EasyMockBeanJupiterTests.class, + MockitoBeanJupiterTests.class, SqlScriptsSpringJupiterTests.class, XmlSpringJupiterTests.class, WebSpringJupiterTests.class); @@ -104,7 +115,16 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); - generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName))); + try { + // Emulate AbstractAotProcessor.process(). + System.setProperty(AbstractAotProcessor.AOT_PROCESSING, "true"); + + generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName))); + } + finally { + // Emulate AbstractAotProcessor.process(). + System.clearProperty(AbstractAotProcessor.AOT_PROCESSING); + } assertRuntimeHints(generator.getRuntimeHints()); @@ -142,6 +162,12 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { else if (testClass.getPackageName().contains("jdbc")) { assertContextForJdbcTests(context); } + else if (testClass.equals(EasyMockBeanJupiterTests.class)) { + assertContextForEasyMockBeanOverrideTests(context); + } + else if (testClass.equals(MockitoBeanJupiterTests.class)) { + assertContextForMockitoBeanOverrideTests(context); + } else { assertContextForBasicTests(context); } @@ -243,6 +269,15 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { .accepts(runtimeHints); assertThat(resource().forResource("org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests.test.sql")) .accepts(runtimeHints); + + // @BeanOverride(value = ...) + assertReflectionRegistered(runtimeHints, "org.springframework.test.context.bean.override.mockito.MockitoBeanOverrideProcessor", + INVOKE_DECLARED_CONSTRUCTORS); + + // GenericApplicationContext.preDetermineBeanTypes() should have registered proxy + // hints for the EasyMock interface-based mocks. + assertProxyRegistered(runtimeHints, GreetingService.class); + assertProxyRegistered(runtimeHints, MessageService.class); } private static void assertReflectionRegistered(RuntimeHints runtimeHints, String type) { @@ -267,6 +302,13 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { assertReflectionRegistered(runtimeHints, annotationType, INVOKE_DECLARED_METHODS); } + private static void assertProxyRegistered(RuntimeHints runtimeHints, Class... interfaces) { + assertThat(proxies().forInterfaces(interfaces)) + .as("Proxy hint for %s", Arrays.asList(interfaces)) + .accepts(runtimeHints); + } + + @Test void processAheadOfTimeWithBasicTests() { @@ -297,6 +339,24 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { assertThat(context.getBean(DataSource.class)).as("DataSource").isNotNull(); } + private void assertContextForEasyMockBeanOverrideTests(ApplicationContext context) { + GreetingService greetingService = context.getBean(GreetingService.class); + MessageService messageService = context.getBean(MessageService.class); + + assertThat(EasyMockSupport.isAMock(greetingService)).as("EasyMock mock").isTrue(); + assertThat(EasyMockSupport.isAMock(messageService)).as("EasyMock mock").isTrue(); + assertThat(Proxy.isProxyClass(greetingService.getClass())).as("JDK proxy").isTrue(); + assertThat(Proxy.isProxyClass(messageService.getClass())).as("JDK proxy").isTrue(); + } + + private void assertContextForMockitoBeanOverrideTests(ApplicationContext context) { + GreetingService greetingService = context.getBean(GreetingService.class); + MessageService messageService = context.getBean(MessageService.class); + + assertThat(Mockito.mockingDetails(greetingService).isMock()).as("Mockito mock").isTrue(); + assertThat(Mockito.mockingDetails(messageService).isMock()).as("Mockito mock").isTrue(); + } + private void assertContextForWebTests(WebApplicationContext wac) throws Exception { assertThat(wac.getEnvironment().getProperty("test.engine")).as("Environment").isNotNull(); @@ -364,10 +424,19 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { testClasses.forEach(testClass -> { DefaultGenerationContext generationContext = generator.createGenerationContext(testClass); MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - ClassName className = generator.processAheadOfTime(mergedConfig, generationContext); - assertThat(className).isNotNull(); - mappings.add(new Mapping(mergedConfig, className)); - generationContext.writeGeneratedContent(); + try { + // Emulate AbstractAotProcessor.process(). + System.setProperty(AbstractAotProcessor.AOT_PROCESSING, "true"); + + ClassName className = generator.processAheadOfTime(mergedConfig, generationContext); + assertThat(className).isNotNull(); + mappings.add(new Mapping(mergedConfig, className)); + generationContext.writeGeneratedContent(); + } + finally { + // Emulate AbstractAotProcessor.process(). + System.clearProperty(AbstractAotProcessor.AOT_PROCESSING); + } }); return mappings; } @@ -422,29 +491,43 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext004_BeanDefinitions.java", - // SqlScriptsSpringJupiterTests + // EasyMockBeanJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_BeanFactoryRegistrations.java", - "org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_BeanFactoryRegistrations.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext005_BeanDefinitions.java", - // WebSpringJupiterTests + // MockitoBeanJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext006_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext006_BeanFactoryRegistrations.java", - "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext006_BeanDefinitions.java", - "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_Autowiring.java", - "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_BeanFactoryRegistrations.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext006_BeanDefinitions.java", - // XmlSpringJupiterTests + // SqlScriptsSpringJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext007_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext007_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java", + "org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext007_BeanDefinitions.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext007_BeanDefinitions.java", + // WebSpringJupiterTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext008_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext008_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext008_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext008_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext008_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext008_BeanDefinitions.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext008_Autowiring.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext008_BeanDefinitions.java", + // XmlSpringJupiterTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext009_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext009_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext009_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext009_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext009_BeanFactoryRegistrations.java", + "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext009_BeanDefinitions.java", }; } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java new file mode 100644 index 0000000000..39d7dddd71 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.aot.samples.bean.override; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.aot.samples.common.GreetingService; +import org.springframework.test.context.aot.samples.common.MessageService; +import org.springframework.test.context.bean.override.easymock.EasyMockBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; + +/** + * @author Sam Brannen + * @since 6.2 + */ +@SpringJUnitConfig +public class EasyMockBeanJupiterTests { + + /** + * Mock for nonexistent bean. + */ + @EasyMockBean + GreetingService greetingService; + + /** + * Mock for existing bean. + */ + @EasyMockBean + MessageService messageService; + + @BeforeEach + void configureMocks(@Autowired GreetingService greetingService, @Autowired MessageService messageService) { + expect(greetingService.greeting()).andReturn("enigma"); + expect(messageService.generateMessage()).andReturn("override"); + replay(greetingService, messageService); + } + + @Test + void test() { + assertThat(greetingService.greeting()).isEqualTo("enigma"); + assertThat(messageService.generateMessage()).isEqualTo("override"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + MessageService messageService() { + return () -> "prod"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java similarity index 51% rename from spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java rename to spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java index 464a5cdce7..9cd784a775 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java @@ -14,42 +14,59 @@ * limitations under the License. */ -package org.springframework.test.context.aot.samples.bean.override.convention; +package org.springframework.test.context.aot.samples.bean.override; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.aot.samples.common.GreetingService; +import org.springframework.test.context.aot.samples.common.MessageService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; /** * @author Sam Brannen * @since 6.2 */ @SpringJUnitConfig -public class TestBeanJupiterTests { +public class MockitoBeanJupiterTests { - @TestBean - String magicBean; + /** + * Mock for nonexistent bean. + */ + @MockitoBean(enforceOverride = false) + GreetingService greetingService; - static String magicBean() { - return "enigma-override"; + /** + * Mock for existing bean. + */ + @MockitoBean + MessageService messageService; + + @BeforeEach + void configureMocks(@Autowired GreetingService greetingService, @Autowired MessageService messageService) { + when(greetingService.greeting()).thenReturn("enigma"); + when(messageService.generateMessage()).thenReturn("override"); } @Test - void tests() { - assertThat(magicBean).isEqualTo("enigma-override"); + void test() { + assertThat(greetingService.greeting()).isEqualTo("enigma"); + assertThat(messageService.generateMessage()).isEqualTo("override"); } @Configuration(proxyBeanMethods = false) static class Config { @Bean - String magicBean() { - return "enigma"; + MessageService messageService() { + return () -> "prod"; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java new file mode 100644 index 0000000000..53e31b7ed9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.aot.samples.common; + +/** + * @author Sam Brannen + * @since 6.2 + */ +@FunctionalInterface +public interface GreetingService { + + String greeting(); + +}