From 7916f7494233584cb9d2cb769dd41adc06adc1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 7 Jun 2024 16:23:07 +0200 Subject: [PATCH] Review TCF cache support for bean override This commit reviews how bean override support can influence the key of an application context cached by the TCF. OverrideMetadata and its subclasses now implement a proper equals/hashCode pair that is tested in various scenarios. Due to how the TCF operates, OverrideMetadata has to be computed in two locations: 1. In a ContextCustomizerFactory, using the metadata in the enclosing class if any. This determines whether a customizer is needed in the first place. The computed set of unique metadata identifies the customizer and participates in the application context cache's key. 2. In the TestExecutionListener so that it knows the override points it has to process. Parsing of the metadata based on a test class has been greatly simplified and moved to OverrideMetadata proper as we don't need several flavors. 1 and 2 are using the same algorithm with the former wrapping that in a Set to compute a proper key. BeanOverrideContextCustomizerEqualityTests provides a framework for testing edge cases as we only care about whether the created ContextCustomizer behaves correctly against the identity of another. Closes gh-32884 --- .../BeanOverrideBeanFactoryPostProcessor.java | 37 +-- .../BeanOverrideContextCustomizer.java | 32 ++- .../BeanOverrideContextCustomizerFactory.java | 24 +- .../override/BeanOverrideParsingUtils.java | 101 ------- .../bean/override/BeanOverrideRegistrar.java | 19 -- .../BeanOverrideTestExecutionListener.java | 13 +- .../bean/override/OverrideMetadata.java | 47 +++- .../convention/TestBeanOverrideMetadata.java | 2 +- .../mockito/MockitoBeanOverrideMetadata.java | 3 +- .../mockito/MockitoOverrideMetadata.java | 3 +- .../MockitoSpyBeanOverrideMetadata.java | 8 +- ...OverrideBeanFactoryPostProcessorTests.java | 4 +- ...verrideContextCustomizerEqualityTests.java | 168 ++++++++++++ ...OverrideContextCustomizerFactoryTests.java | 144 ++++++++++ .../BeanOverrideContextCustomizerTests.java | 103 ++++++++ .../BeanOverrideParsingUtilsTests.java | 118 --------- .../bean/override/OverrideMetadataTests.java | 249 ++++++++++++++++-- ...ngTestBeanInheritanceIntegrationTests.java | 7 +- .../FailingTestBeanIntegrationTests.java | 9 +- .../TestBeanOverrideMetadataTests.java | 178 +++++++++++++ .../MockitoBeanByTypeIntegrationTests.java | 11 +- .../MockitoBeanOverrideMetadataTests.java | 151 +++++++++++ .../MockitoSpyBeanOverrideMetadataTests.java | 139 ++++++++++ 23 files changed, 1239 insertions(+), 331 deletions(-) delete mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerEqualityTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java 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 1ee455db2fe..90229ad7889 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 @@ -41,38 +41,41 @@ import org.springframework.core.ResolvableType; import org.springframework.util.StringUtils; /** - * A {@link BeanFactoryPostProcessor} implementation that processes test classes - * and adapts the {@link BeanFactory} for any {@link BeanOverride} it may define. + * A {@link BeanFactoryPostProcessor} implementation that processes identified + * use of {@link BeanOverride} and adapts the {@link BeanFactory} accordingly. * - *

A set of classes from which to parse {@link OverrideMetadata} must be - * provided to this processor. Each test class is expected to use any - * annotation meta-annotated with {@link BeanOverride @BeanOverride} to mark - * beans to override. The {@link BeanOverrideParsingUtils#hasBeanOverride(Class)} - * method can be used to check if a class matches the above criteria. + *

For each override, the bean factory is prepared according to the chosen + * {@link BeanOverrideStrategy overriding strategy}. The override value is created, + * if necessary, and the necessary infrastructure is updated to allow the value + * to be injected in the corresponding {@linkplain OverrideMetadata#getField() field} + * of the test class. * - *

The provided classes are fully parsed at creation to build a metadata set. - * This processor implements several {@link BeanOverrideStrategy overriding - * strategies} and chooses the correct one according to each override metadata's - * {@link OverrideMetadata#getStrategy()} method. Additionally, it provides - * support for injecting the overridden bean instances into their corresponding - * annotated {@link Field fields}. + *

This processor does not work against a particular test class, it only prepares + * the bean factory for the identified, unique, set of bean overrides. * * @author Simon Baslé + * @author Stephane Nicoll * @since 6.2 */ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { + private final Set metadata; + private final BeanOverrideRegistrar overrideRegistrar; /** * Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with - * the given {@link BeanOverrideRegistrar}, which contains a set of parsed - * {@link OverrideMetadata}. + * the set of {@link OverrideMetadata} to process, using the given + * {@link BeanOverrideRegistrar}. + * @param metadata the {@link OverrideMetadata} instances to process * @param overrideRegistrar the {@link BeanOverrideRegistrar} used to track * metadata */ - public BeanOverrideBeanFactoryPostProcessor(BeanOverrideRegistrar overrideRegistrar) { + public BeanOverrideBeanFactoryPostProcessor(Set metadata, + BeanOverrideRegistrar overrideRegistrar) { + + this.metadata = metadata; this.overrideRegistrar = overrideRegistrar; } @@ -92,7 +95,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, } private void postProcessWithRegistry(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) { - for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) { + for (OverrideMetadata metadata : this.metadata) { registerBeanOverride(beanFactory, registry, metadata); } } 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 423a41a98c0..d74740aa8b8 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 @@ -49,10 +49,10 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { "org.springframework.test.context.bean.override.internalWrapEarlyBeanPostProcessor"; - private final Set> detectedClasses; + private final Set metadata; - BeanOverrideContextCustomizer(Set> detectedClasses) { - this.detectedClasses = detectedClasses; + BeanOverrideContextCustomizer(Set metadata) { + this.metadata = metadata; } @Override @@ -61,19 +61,25 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { throw new IllegalStateException("Cannot process bean overrides with an ApplicationContext " + "that doesn't implement BeanDefinitionRegistry: " + context.getClass()); } - registerInfrastructure(registry, this.detectedClasses); + registerInfrastructure(registry); } - private void registerInfrastructure(BeanDefinitionRegistry registry, Set> detectedClasses) { + Set getMetadata() { + return this.metadata; + } + + private void registerInfrastructure(BeanDefinitionRegistry registry) { addInfrastructureBeanDefinition(registry, BeanOverrideRegistrar.class, REGISTRAR_BEAN_NAME, - constructorArgs -> constructorArgs.addIndexedArgumentValue(0, detectedClasses)); + 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, + 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, @@ -97,12 +103,12 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { return false; } BeanOverrideContextCustomizer that = (BeanOverrideContextCustomizer) other; - return this.detectedClasses.equals(that.detectedClasses); + return this.metadata.equals(that.metadata); } @Override public int hashCode() { - return this.detectedClasses.hashCode(); + return this.metadata.hashCode(); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index 73b072a62be..4b79f97d456 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -16,13 +16,12 @@ package org.springframework.test.context.bean.override; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; -import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.TestContextAnnotationUtils; @@ -31,6 +30,7 @@ import org.springframework.test.context.TestContextAnnotationUtils; * Bean Overriding. * * @author Simon Baslé + * @author Stephane Nicoll * @since 6.2 * @see BeanOverride */ @@ -38,24 +38,22 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { @Override @Nullable - public ContextCustomizer createContextCustomizer(Class testClass, + public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - Set> detectedClasses = new LinkedHashSet<>(); - findClassesWithBeanOverride(testClass, detectedClasses); - if (detectedClasses.isEmpty()) { + Set metadata = new HashSet<>(); + findOverrideMetadata(testClass, metadata); + if (metadata.isEmpty()) { return null; } - - return new BeanOverrideContextCustomizer(detectedClasses); + return new BeanOverrideContextCustomizer(metadata); } - private void findClassesWithBeanOverride(Class testClass, Set> detectedClasses) { - if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) { - detectedClasses.add(testClass); - } + private void findOverrideMetadata(Class testClass, Set metadata) { + List overrideMetadata = OverrideMetadata.forTestClass(testClass); + metadata.addAll(overrideMetadata); if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { - findClassesWithBeanOverride(testClass.getEnclosingClass(), detectedClasses); + findOverrideMetadata(testClass.getEnclosingClass(), metadata); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java deleted file mode 100644 index 3c0984ec8c8..00000000000 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.annotation.Annotation; -import java.lang.reflect.Field; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.springframework.beans.BeanUtils; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; - -import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; - -/** - * Internal parsing utilities to discover the presence of - * {@link BeanOverride @BeanOverride} and create the relevant - * {@link OverrideMetadata} if necessary. - * - * @author Simon Baslé - * @author Sam Brannen - * @since 6.2 - */ -abstract class BeanOverrideParsingUtils { - - /** - * Check if at least one field of the given {@code clazz} is meta-annotated - * with {@link BeanOverride @BeanOverride}. - * @param clazz the class which fields to inspect - * @return {@code true} if there is a bean override annotation present, - * {@code false} otherwise - */ - static boolean hasBeanOverride(Class clazz) { - AtomicBoolean hasBeanOverride = new AtomicBoolean(); - ReflectionUtils.doWithFields(clazz, field -> { - if (hasBeanOverride.get()) { - return; - } - boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class); - hasBeanOverride.compareAndSet(false, present); - }); - return hasBeanOverride.get(); - } - - /** - * Parse the specified classes for the presence of fields annotated with - * {@link BeanOverride @BeanOverride}, and create an {@link OverrideMetadata} - * for each identified field. - * @param classes the classes to parse - */ - static Set parse(Iterable> classes) { - Set result = new LinkedHashSet<>(); - classes.forEach(c -> ReflectionUtils.doWithFields(c, field -> parseField(field, c, result))); - return result; - } - - /** - * Convenience method to parse a single test class. - * @see #parse(Iterable) - */ - static Set parse(Class clazz) { - return parse(List.of(clazz)); - } - - private static void parseField(Field field, Class testClass, Set metadataSet) { - AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { - MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); - Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present"); - - BeanOverride beanOverride = mergedAnnotation.synthesize(); - BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); - Annotation composedAnnotation = metaSource.synthesize(); - - Assert.state(overrideAnnotationFound.compareAndSet(false, true), - () -> "Multiple @BeanOverride annotations found on field: " + field); - OverrideMetadata metadata = processor.createMetadata(composedAnnotation, testClass, field); - metadataSet.add(metadata); - }); - } - -} 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 e8a8bfd30d1..2dafdcf23cf 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 @@ -19,7 +19,6 @@ package org.springframework.test.context.bean.override; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; -import java.util.Set; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; @@ -45,21 +44,10 @@ class BeanOverrideRegistrar implements BeanFactoryAware { private final Map earlyOverrideMetadata = new HashMap<>(); - private final Set overrideMetadata; - @Nullable private ConfigurableBeanFactory beanFactory; - /** - * Create a new registrar and immediately parse the provided classes. - * @param classesToParse the initial set of classes that have been - * detected to contain bean overriding annotations - */ - BeanOverrideRegistrar(Set> classesToParse) { - this.overrideMetadata = BeanOverrideParsingUtils.parse(classesToParse); - } - @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) { @@ -69,13 +57,6 @@ class BeanOverrideRegistrar implements BeanFactoryAware { this.beanFactory = cbf; } - /** - * Get the detected {@link OverrideMetadata} instances. - */ - Set getOverrideMetadata() { - return this.overrideMetadata; - } - /** * Check {@link #markWrapEarly(OverrideMetadata, String) early override} * records and use the {@link OverrideMetadata} to create an override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java index e84b65cf422..10a59a5d301 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java @@ -17,6 +17,7 @@ package org.springframework.test.context.bean.override; import java.lang.reflect.Field; +import java.util.List; import java.util.function.BiConsumer; import org.springframework.test.context.TestContext; @@ -74,12 +75,12 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList if (Boolean.TRUE.equals( testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { - postProcessFields(testContext, (testMetadata, postProcessor) -> { + postProcessFields(testContext, (testMetadata, registrar) -> { Object testInstance = testMetadata.testInstance; Field field = testMetadata.overrideMetadata.getField(); ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, testInstance, null); - postProcessor.inject(testInstance, testMetadata.overrideMetadata); + registrar.inject(testInstance, testMetadata.overrideMetadata); }); } } @@ -90,13 +91,11 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList Class testClass = testContext.getTestClass(); Object testInstance = testContext.getTestInstance(); - if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) { + List metadataForFields = OverrideMetadata.forTestClass(testClass); + if (!metadataForFields.isEmpty()) { BeanOverrideRegistrar registrar = testContext.getApplicationContext().getBean(BeanOverrideRegistrar.class); - for (OverrideMetadata metadata : registrar.getOverrideMetadata()) { - if (!metadata.getField().getDeclaringClass().isAssignableFrom(testClass)) { - continue; - } + for (OverrideMetadata metadata : metadataForFields) { consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), registrar); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java index 08510af51bb..c7a323e354a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java @@ -16,14 +16,26 @@ package org.springframework.test.context.bean.override; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; /** * Metadata for Bean Override injection points, also responsible for creation of @@ -61,6 +73,34 @@ public abstract class OverrideMetadata { this.strategy = strategy; } + /** + * Parse the given {@code testClass} and provide the use of bean override. + * @param testClass the class to parse + * @return a list of bean overrides metadata + */ + public static List forTestClass(Class testClass) { + List all = new LinkedList<>(); + ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass, all)); + return all; + } + + private static void parseField(Field field, Class testClass, List metadataList) { + AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); + MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present"); + + BeanOverride beanOverride = mergedAnnotation.synthesize(); + BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); + Annotation composedAnnotation = metaSource.synthesize(); + + Assert.state(overrideAnnotationFound.compareAndSet(false, true), + () -> "Multiple @BeanOverride annotations found on field: " + field); + OverrideMetadata metadata = processor.createMetadata(composedAnnotation, testClass, field); + metadataList.add(metadata); + }); + } + /** * Get the annotated {@link Field}. @@ -127,15 +167,16 @@ public abstract class OverrideMetadata { return false; } OverrideMetadata that = (OverrideMetadata) obj; - return Objects.equals(this.beanType, that.beanType) && + return Objects.equals(this.beanType.getType(), that.beanType.getType()) && Objects.equals(this.beanName, that.beanName) && Objects.equals(this.strategy, that.strategy) && - Objects.equals(this.field, that.field); + Arrays.equals(this.field.getAnnotations(), that.field.getAnnotations()); } @Override public int hashCode() { - return Objects.hash(this.beanType, this.beanName, this.strategy, this.field); + return Objects.hash(this.beanType.getType(), this.beanName, this.strategy, + Arrays.hashCode(this.field.getAnnotations())); } @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java index b9510822380..675094baff2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java @@ -79,6 +79,6 @@ final class TestBeanOverrideMetadata extends OverrideMetadata { @Override public int hashCode() { - return Objects.hash(super.hashCode(), this.overrideMethod); + return this.overrideMethod.hashCode() * 29 + super.hashCode(); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java index de5822a9245..a4e7e5a14c1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.mock; * {@link OverrideMetadata} implementation for Mockito {@code mock} support. * * @author Phillip Webb + * @author Stephane Nicoll * @since 6.2 */ class MockitoBeanOverrideMetadata extends MockitoOverrideMetadata { @@ -142,7 +143,7 @@ class MockitoBeanOverrideMetadata extends MockitoOverrideMetadata { @Override public int hashCode() { - return Objects.hash(super.hashCode(), this.extraInterfaces, this.answer, this.serializable); + return Objects.hash(this.extraInterfaces, this.answer, this.serializable) + super.hashCode(); } @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoOverrideMetadata.java index d2c06a4794c..d024a105b56 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoOverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoOverrideMetadata.java @@ -31,6 +31,7 @@ import org.springframework.util.ObjectUtils; * Base {@link OverrideMetadata} implementation for Mockito. * * @author Phillip Webb + * @author Stephane Nicoll * @since 6.2 */ abstract class MockitoOverrideMetadata extends OverrideMetadata { @@ -98,7 +99,7 @@ abstract class MockitoOverrideMetadata extends OverrideMetadata { @Override public int hashCode() { - return Objects.hash(super.hashCode(), this.reset, this.proxyTargetAware); + return Objects.hash(this.reset, this.proxyTargetAware) + super.hashCode(); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadata.java index 8910472e309..5078a5dab59 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadata.java @@ -18,7 +18,6 @@ package org.springframework.test.context.bean.override.mockito; import java.lang.reflect.Field; import java.lang.reflect.Proxy; -import java.util.Objects; import org.mockito.AdditionalAnswers; import org.mockito.MockSettings; @@ -34,7 +33,6 @@ import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.test.context.bean.override.OverrideMetadata; import org.springframework.test.util.AopTestUtils; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import static org.mockito.Mockito.mock; @@ -44,6 +42,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Simon Baslé + * @author Stephane Nicoll * @since 6.2 */ class MockitoSpyBeanOverrideMetadata extends MockitoOverrideMetadata { @@ -108,13 +107,12 @@ class MockitoSpyBeanOverrideMetadata extends MockitoOverrideMetadata { if (other == null || other.getClass() != getClass()) { return false; } - MockitoSpyBeanOverrideMetadata that = (MockitoSpyBeanOverrideMetadata) other; - return (super.equals(that) && ObjectUtils.nullSafeEquals(getBeanType(), that.getBeanType())); + return super.equals(other); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), getBeanType()); + return getClass().hashCode() * 29 + super.hashCode(); } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java index e2805cf0885..b603cb6df40 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java @@ -17,6 +17,7 @@ package org.springframework.test.context.bean.override; import java.lang.reflect.Field; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -203,7 +204,8 @@ class BeanOverrideBeanFactoryPostProcessorTests { private AnnotationConfigApplicationContext createContext(Class testClass) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - new BeanOverrideContextCustomizer(Set.of(testClass)).customizeContext(context, mock(MergedContextConfiguration.class)); + Set metadata = new LinkedHashSet<>(OverrideMetadata.forTestClass(testClass)); + new BeanOverrideContextCustomizer(metadata).customizeContext(context, mock(MergedContextConfiguration.class)); return context; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerEqualityTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerEqualityTests.java new file mode 100644 index 00000000000..289d2395b59 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerEqualityTests.java @@ -0,0 +1,168 @@ +/* + * 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.util.Collections; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; + +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Bean override tests that validate the behavior with the TCF context cache. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +class BeanOverrideContextCustomizerEqualityTests { + + private final BeanOverrideContextCustomizerFactory factory = new BeanOverrideContextCustomizerFactory(); + + @Nested + class SameContextTests { + + @Test + void testsWithOneIdenticalTestBean() { + assertThat(createContextCustomizer(Case1.class)).isEqualTo(createContextCustomizer(Case2.class)); + } + + @Test + void testsWithOneIdenticalMockitoMockBean() { + assertThat(createContextCustomizer(Case4.class)).isEqualTo(createContextCustomizer(Case5.class)); + } + + @Test + void testsWithOneIdenticalMockitoSpyBean() { + assertThat(createContextCustomizer(Case7.class)).isEqualTo(createContextCustomizer(Case8.class)); + } + } + + @Nested + class DifferentContextTests { + + @Test + void testsWithSimilarTestBeanButDifferentMethod() { + assertThat(createContextCustomizer(Case1.class)).isNotEqualTo(createContextCustomizer(Case3.class)); + } + + @Test + void testsWithSimilarMockitoMockButDifferentAnswers() { + assertThat(createContextCustomizer(Case4.class)).isNotEqualTo(createContextCustomizer(Case6.class)); + } + + @Test + void testsWithSimilarMockitoSpyButDifferentProxyTargetClass() { + assertThat(createContextCustomizer(Case8.class)).isNotEqualTo(createContextCustomizer(Case9.class)); + } + + @Test + void testsWithSameConfigurationButOneIsMockitoBeanAndTheOtherMockitoSpy() { + assertThat(createContextCustomizer(Case4.class)).isNotEqualTo(createContextCustomizer(Case7.class)); + } + + } + + + private ContextCustomizer createContextCustomizer(Class testClass) { + BeanOverrideContextCustomizer customizer = this.factory.createContextCustomizer( + testClass, Collections.emptyList()); + assertThat(customizer).isNotNull(); + return customizer; + } + + interface DescriptionProvider { + + static String createDescription() { + return "override"; + } + + } + + static class Case1 implements DescriptionProvider { + + @TestBean(methodName = "createDescription") + private String description; + + } + + static class Case2 implements DescriptionProvider { + + @TestBean(methodName = "createDescription") + private String description; + + } + + static class Case3 implements DescriptionProvider { + + @TestBean(methodName = "createDescription") + private String description; + + static String createDescription() { + return "another value"; + } + } + + static class Case4 { + + @MockitoBean + private String exampleService; + + } + + static class Case5 { + + @MockitoBean + private String serviceToMock; + + } + + static class Case6 { + + @MockitoBean(answers = Answers.RETURNS_MOCKS) + private String exampleService; + + } + + static class Case7 { + + @MockitoSpyBean + private String exampleService; + + } + + static class Case8 { + + @MockitoSpyBean + private String serviceToMock; + + } + + static class Case9 { + + @MockitoSpyBean(proxyTargetAware = false) + private String serviceToMock; + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java new file mode 100644 index 00000000000..d26473270a5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java @@ -0,0 +1,144 @@ +/* + * 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.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactoryTests.Test2.Green; +import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactoryTests.Test2.Orange; +import org.springframework.test.context.bean.override.convention.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link BeanOverrideContextCustomizerFactory}. + * + * @author Stephane Nicoll + */ +class BeanOverrideContextCustomizerFactoryTests { + + private final BeanOverrideContextCustomizerFactory factory = new BeanOverrideContextCustomizerFactory(); + + @Test + void createContextCustomizerWhenTestHasNoBeanOverride() { + assertThat(createContextCustomizer(String.class)).isNull(); + } + + @Test + void createContextCustomizerWhenTestHasSingleBeanOverride() { + BeanOverrideContextCustomizer customizer = createContextCustomizer(Test1.class); + assertThat(customizer).isNotNull(); + assertThat(customizer.getMetadata()).singleElement().satisfies(testMetadata(null, String.class)); + } + + @Test + void createContextCustomizerWhenNestedTestHasSingleBeanOverrideInParent() { + BeanOverrideContextCustomizer customizer = createContextCustomizer(Orange.class); + assertThat(customizer).isNotNull(); + assertThat(customizer.getMetadata()).singleElement().satisfies(testMetadata(null, String.class)); + } + + @Test + void createContextCustomizerWhenNestedTestHasBeanOverrideAsWellAsTheParent() { + BeanOverrideContextCustomizer customizer = createContextCustomizer(Green.class); + assertThat(customizer).isNotNull(); + assertThat(customizer.getMetadata()) + .anySatisfy(testMetadata(null, String.class)) + .anySatisfy(testMetadata("counterBean", Integer.class)) + .hasSize(2); + } + + @Test + void createContextCustomizerWhenTestHasInvalidTestBeanTargetMethod() { + assertThatIllegalStateException() + .isThrownBy(() -> createContextCustomizer(InvalidTestMissingMethod.class)) + .withMessageContaining("Failed to find a static test bean factory method"); + } + + + private Consumer testMetadata(@Nullable String beanName, Class beanType) { + return overrideMetadata(beanName, beanType, BeanOverrideStrategy.REPLACE_DEFINITION); + } + + private Consumer overrideMetadata(@Nullable String beanName, Class beanType, BeanOverrideStrategy strategy) { + return metadata -> { + assertThat(metadata.getBeanName()).isEqualTo(beanName); + assertThat(metadata.getBeanType().toClass()).isEqualTo(beanType); + assertThat(metadata.getStrategy()).isEqualTo(strategy); + }; + } + + @Nullable + BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { + return this.factory.createContextCustomizer(testClass, Collections.emptyList()); + } + + static class Test1 { + + @TestBean(methodName = "descriptor") + private String descriptor; + + private static String descriptor() { + return "Overridden descriptor"; + } + + } + + static class Test2 { + + @TestBean(methodName = "name") + private String name; + + private static String name() { + return "Overridden name"; + } + + @Nested + class Orange { + + } + + @Nested + class Green { + + @TestBean(name = "counterBean", methodName = "counter") + private Integer counter; + + private static Integer counter() { + return 42; + } + } + } + + static class InvalidTestMissingMethod { + + @TestBean(methodName = "descriptor") + private String descriptor; + + private String descriptor() { + return "Never called, not static"; + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java new file mode 100644 index 00000000000..f518d08ea58 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java @@ -0,0 +1,103 @@ +/* + * 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.Field; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BeanOverrideContextCustomizer}. + * + * @author Stephane Nicoll + */ +class BeanOverrideContextCustomizerTests { + + @Test + void customizerIsEqualWithIdenticalMetadata() { + BeanOverrideContextCustomizer customizer = createCustomizer(new DummyOverrideMetadata("key")); + BeanOverrideContextCustomizer customizer2 = createCustomizer(new DummyOverrideMetadata("key")); + assertThat(customizer).isEqualTo(customizer2); + assertThat(customizer).hasSameHashCodeAs(customizer2); + } + + @Test + void customizerIsEqualWithIdenticalMetadataInDifferentOrder() { + BeanOverrideContextCustomizer customizer = createCustomizer( + new DummyOverrideMetadata("key1"), new DummyOverrideMetadata("key2")); + BeanOverrideContextCustomizer customizer2 = createCustomizer( + new DummyOverrideMetadata("key2"), new DummyOverrideMetadata("key1")); + assertThat(customizer).isEqualTo(customizer2); + assertThat(customizer).hasSameHashCodeAs(customizer2); + } + + @Test + void customizerIsNotEqualWithDifferentMetadata() { + BeanOverrideContextCustomizer customizer = createCustomizer(new DummyOverrideMetadata("key")); + BeanOverrideContextCustomizer customizer2 = createCustomizer( + new DummyOverrideMetadata("key"), new DummyOverrideMetadata("another")); + assertThat(customizer).isNotEqualTo(customizer2); + } + + private BeanOverrideContextCustomizer createCustomizer(OverrideMetadata... metadata) { + return new BeanOverrideContextCustomizer(new LinkedHashSet<>(Arrays.asList(metadata))); + } + + private static class DummyOverrideMetadata extends OverrideMetadata { + + private final String key; + + public DummyOverrideMetadata(String key) { + super(mock(Field.class), ResolvableType.forClass(Object.class), null, BeanOverrideStrategy.REPLACE_DEFINITION); + this.key = key; + } + + @Override + protected Object createOverride(String beanName, BeanDefinition existingBeanDefinition, + Object existingBeanInstance) { + return existingBeanInstance; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DummyOverrideMetadata that = (DummyOverrideMetadata) o; + return Objects.equals(this.key, that.key); + } + + @Override + public int hashCode() { + return this.key.hashCode(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java deleted file mode 100644 index ef134d292ff..00000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.Field; - -import org.junit.jupiter.api.Test; - -import org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation; -import org.springframework.test.context.bean.override.example.TestBeanOverrideMetaAnnotation; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatRuntimeException; - -/** - * Unit tests for {@link BeanOverrideParsingUtils}. - * - * @since 6.2 - */ -class BeanOverrideParsingUtilsTests { - - // Metadata built from a String that starts with DUPLICATE_TRIGGER are considered equal - private static final String DUPLICATE_TRIGGER1 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v1"; - private static final String DUPLICATE_TRIGGER2 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v2"; - - @Test - void findsOnField() { - assertThat(BeanOverrideParsingUtils.parse(SingleAnnotationOnField.class)) - .map(Object::toString) - .containsExactly("onField"); - } - - @Test - void allowsMultipleProcessorsOnDifferentElements() { - assertThat(BeanOverrideParsingUtils.parse(AnnotationsOnMultipleFields.class)) - .map(Object::toString) - .containsExactlyInAnyOrder("onField1", "onField2"); - } - - @Test - void rejectsMultipleAnnotationsOnSameElement() { - Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message"); - assertThatRuntimeException() - .isThrownBy(() -> BeanOverrideParsingUtils.parse(MultipleAnnotationsOnField.class)) - .withMessage("Multiple @BeanOverride annotations found on field: " + field); - } - - @Test - void keepsFirstOccurrenceOfEqualMetadata() { - assertThat(BeanOverrideParsingUtils.parse(DuplicateConf.class)) - .map(Object::toString) - .containsExactly("{DUPLICATE-v1}"); - } - - - static class SingleAnnotationOnField { - - @ExampleBeanOverrideAnnotation("onField") - String message; - - static String onField() { - return "OK"; - } - } - - static class MultipleAnnotationsOnField { - - @ExampleBeanOverrideAnnotation("foo") - @TestBeanOverrideMetaAnnotation - String message; - - static String foo() { - return "foo"; - } - } - - static class AnnotationsOnMultipleFields { - - @ExampleBeanOverrideAnnotation("onField1") - String message; - - @ExampleBeanOverrideAnnotation("onField2") - String messageOther; - - static String onField1() { - return "OK1"; - } - - static String onField2() { - return "OK2"; - } - } - - static class DuplicateConf { - - @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER1) - String message1; - - @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER2) - String message2; - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index 7217d9a902d..8724b393130 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -16,47 +16,266 @@ package org.springframework.test.context.bean.override; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.List; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.example.CustomQualifier; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link OverrideMetadata}. * * @author Simon Baslé + * @author Stephane Nicoll * @since 6.2 */ -class OverrideMetadataTests { +public class OverrideMetadataTests { @Test - void implicitConfigurations() throws Exception { - OverrideMetadata metadata = exampleOverride(); - assertThat(metadata.getBeanName()).as("expectedBeanName").isNull(); + void forTestClassWithSingleField() { + List overrideMetadata = OverrideMetadata.forTestClass(SingleAnnotation.class); + assertThat(overrideMetadata).singleElement().satisfies(hasTestBeanMetadata( + field(SingleAnnotation.class, "message"), String.class, null)); } + @Test + void forTestClassWithMultipleFields() { + List overrideMetadata = OverrideMetadata.forTestClass(MultipleAnnotations.class); + assertThat(overrideMetadata).hasSize(2) + .anySatisfy(hasTestBeanMetadata( + field(MultipleAnnotations.class, "message"), String.class, null)) + .anySatisfy(hasTestBeanMetadata( + field(MultipleAnnotations.class, "counter"), Integer.class, null)); + } + + @Test + void forTestClassWithMultipleFieldsSameMetadata() { + List overrideMetadata = OverrideMetadata.forTestClass(MultipleAnnotationsDuplicate.class); + assertThat(overrideMetadata).hasSize(2) + .anySatisfy(hasTestBeanMetadata( + field(MultipleAnnotationsDuplicate.class, "message1"), String.class, null)) + .anySatisfy(hasTestBeanMetadata( + field(MultipleAnnotationsDuplicate.class, "message2"), String.class, null)); + assertThat(new HashSet<>(overrideMetadata)).hasSize(1); + } + + @Test + void forTestClassWithDifferentOverrideMetadataOnSameField() { + Field faultyField = field(MultipleAnnotationsOnSameField.class, "message"); + assertThatIllegalStateException() + .isThrownBy(() -> OverrideMetadata.forTestClass(MultipleAnnotationsOnSameField.class)) + .withMessageStartingWith("Multiple @BeanOverride annotations found") + .withMessageContaining(faultyField.toString()); + } + + @Test + void getBeanNameIsNullByDefault() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier")); + assertThat(metadata.getBeanName()).isNull(); + } + + @Test + void isEqualToWithSameInstance() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier")); + assertThat(metadata).isEqualTo(metadata); + assertThat(metadata).hasSameHashCodeAs(metadata); + } + + @Test + void isEqualToWithSameMetadata() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigA.class, "noQualifier")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isEqualToWithSameMetadataAndBeanNames() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier"), "testBean"); + OverrideMetadata metadata2 = createMetadata(field(ConfigA.class, "noQualifier"), "testBean"); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataAndDifferentBeaName() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier"), "testBean"); + OverrideMetadata metadata2 = createMetadata(field(ConfigA.class, "noQualifier"), "testBean2"); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isEqualToWithSameMetadataButDifferentFields() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigB.class, "noQualifier")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isEqualToWithSameMetadataAndSameQualifierValues() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "directQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigB.class, "directQualifier")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataAndDifferentQualifierValues() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "directQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigA.class, "differentDirectQualifier")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataAndDifferentQualifiers() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "directQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigA.class, "customQualifier")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + private OverrideMetadata createMetadata(Field field) { + return createMetadata(field, null); + } + + private OverrideMetadata createMetadata(Field field, @Nullable String name) { + return new DummyOverrideMetadata(field, field.getType(), name); + } + + private Field field(Class target, String fieldName) { + Field field = ReflectionUtils.findField(target, fieldName); + assertThat(field).isNotNull(); + return field; + } + + private Consumer hasTestBeanMetadata(Field field, Class beanType, @Nullable String beanName) { + return hasOverrideMetadata(field, beanType, BeanOverrideStrategy.REPLACE_DEFINITION, beanName); + } + + private Consumer hasOverrideMetadata(Field field, Class beanType, BeanOverrideStrategy strategy, @Nullable String beanName) { + return metadata -> { + assertThat(metadata.getField()).isEqualTo(field); + assertThat(metadata.getBeanType().toClass()).isEqualTo(beanType); + assertThat(metadata.getStrategy()).isEqualTo(strategy); + assertThat(metadata.getBeanName()).isEqualTo(beanName); + }; + } + + + static class SingleAnnotation { + + @DummyBean + String message; + + } + + static class MultipleAnnotations { + + @DummyBean + String message; + + @DummyBean + Integer counter; + } + + static class MultipleAnnotationsDuplicate { + + @DummyBean + String message1; + + @DummyBean + String message2; + + } + + static class MultipleAnnotationsOnSameField { + + @MetaDummyBean() + @DummyBean + String message; + + static String foo() { + return "foo"; + } + } + + public static class ConfigA { + + private ExampleService noQualifier; - @NonNull - String annotated = "exampleField"; + @Qualifier("test") + private ExampleService directQualifier; + + @Qualifier("different") + private ExampleService differentDirectQualifier; + + @CustomQualifier + private ExampleService customQualifier; - private static OverrideMetadata exampleOverride() throws Exception { - Field field = OverrideMetadataTests.class.getDeclaredField("annotated"); - return new ConcreteOverrideMetadata(field, ResolvableType.forClass(String.class), - BeanOverrideStrategy.REPLACE_DEFINITION); } - static class ConcreteOverrideMetadata extends OverrideMetadata { + public static class ConfigB { + + private ExampleService noQualifier; + + @Qualifier("test") + private ExampleService directQualifier; + + } + + // Simple OverrideMetadata implementation + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @DummyBean + public @interface MetaDummyBean {} - ConcreteOverrideMetadata(Field field, ResolvableType typeToOverride, - BeanOverrideStrategy strategy) { + @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @BeanOverride(DummyBeanOverrideProcessor.class) + public @interface DummyBean {} - super(field, typeToOverride, null, strategy); + static class DummyBeanOverrideProcessor implements BeanOverrideProcessor { + + @Override + public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class testClass, Field field) { + return new DummyOverrideMetadata(field, field.getType(), null); + } + } + + static class DummyOverrideMetadata extends OverrideMetadata { + + @Nullable + private final String beanName; + + DummyOverrideMetadata(Field field, Class typeToOverride, @Nullable String beanName) { + super(field, ResolvableType.forClass(typeToOverride), beanName, BeanOverrideStrategy.REPLACE_DEFINITION); + this.beanName = beanName; + } + + @Override + @Nullable + public String getBeanName() { + return this.beanName; } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanInheritanceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanInheritanceIntegrationTests.java index b02ced0a63d..c6289d6323e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanInheritanceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanInheritanceIntegrationTests.java @@ -23,7 +23,6 @@ import org.springframework.test.context.junit.EngineTestKitUtils; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; -import static org.springframework.test.context.junit.EngineTestKitUtils.rootCause; /** * {@link TestBean @TestBean} inheritance integration tests for failure scenarios. @@ -40,12 +39,11 @@ class FailingTestBeanInheritanceIntegrationTests { Class testClass = FieldInSupertypeButNoMethodTestCase.class; EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1, finishedWithFailure( - rootCause( instanceOf(IllegalStateException.class), message(""" Failed to find a static test bean factory method in %s with return type %s \ whose name matches one of the supported candidates [someBeanTestOverride]""" - .formatted(testClass.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()))))); + .formatted(testClass.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName())))); } @Test @@ -53,13 +51,12 @@ class FailingTestBeanInheritanceIntegrationTests { Class testClass = Method1InSupertypeAndMethod2InTypeTestCase.class; EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1, finishedWithFailure( - rootCause( instanceOf(IllegalStateException.class), message(""" Found 2 competing static test bean factory methods in %s with return type %s \ whose name matches one of the supported candidates \ [anotherBeanTestOverride, thirdBeanTestOverride]""" - .formatted(testClass.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()))))); + .formatted(testClass.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName())))); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanIntegrationTests.java index 9c04f8a34a5..f7958c4f457 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanIntegrationTests.java @@ -69,25 +69,22 @@ class FailingTestBeanIntegrationTests { Class testClass = ExplicitTestOverrideMethodNotPresentTestCase.class; EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1, finishedWithFailure( - rootCause( instanceOf(IllegalStateException.class), message(""" Failed to find a static test bean factory method in %s with return type \ java.lang.String whose name matches one of the supported candidates \ - [notPresent]""".formatted(testClass.getName()))))); + [notPresent]""".formatted(testClass.getName())))); } @Test void testBeanFailingNoExplicitMethod() { Class testClass = ImplicitTestOverrideMethodNotPresentTestCase.class; EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1, - finishedWithFailure( - rootCause( - instanceOf(IllegalStateException.class), + finishedWithFailure(instanceOf(IllegalStateException.class), message(""" Failed to find a static test bean factory method in %s with return type \ java.lang.String whose name matches one of the supported candidates \ - [fieldTestOverride]""".formatted(testClass.getName()))))); + [fieldTestOverride]""".formatted(testClass.getName())))); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java new file mode 100644 index 00000000000..d3643181440 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java @@ -0,0 +1,178 @@ +/* + * 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.convention; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.ResolvableType; +import org.springframework.test.context.bean.override.OverrideMetadata; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link TestBeanOverrideMetadata}. + * + * @author Stephane Nicoll + */ +class TestBeanOverrideMetadataTests { + + @Test + void forTestClassSetsNameToNullIfAnnotationNameIsNull() { + List list = OverrideMetadata.forTestClass(SampleOneOverride.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isNull()); + } + + @Test + void forTestClassSetsNameToAnnotationName() { + List list = OverrideMetadata.forTestClass(SampleOneOverrideWithName.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isEqualTo("anotherBean")); + } + + @Test + void forTestClassWithMissingMethod() { + assertThatIllegalStateException() + .isThrownBy(() ->OverrideMetadata.forTestClass(SampleMissingMethod.class)) + .withMessageStartingWith("Failed to find a static test bean factory method") + .withMessageContaining("messageTestOverride"); + } + + @Test + void isEqualToWithSameInstance() { + TestBeanOverrideMetadata metadata = createMetadata(sampleField("message"), sampleMethod("message")); + assertThat(metadata).isEqualTo(metadata); + assertThat(metadata).hasSameHashCodeAs(metadata); + } + + @Test + void isEqualToWithSameMetadata() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message"), sampleMethod("message")); + assertThat(metadata1).isEqualTo(metadata2); + assertThat(metadata1).hasSameHashCodeAs(metadata2); + } + + @Test + void isEqualToWithSameMetadataButDifferentField() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message2"), sampleMethod("message")); + assertThat(metadata1).isEqualTo(metadata2); + assertThat(metadata1).hasSameHashCodeAs(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentBeanName() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message3"), sampleMethod("message")); + assertThat(metadata1).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentMethod() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message"), sampleMethod("description")); + assertThat(metadata1).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentAnnotations() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message4"), sampleMethod("message")); + assertThat(metadata1).isNotEqualTo(metadata2); + } + + private Field sampleField(String fieldName) { + Field field = ReflectionUtils.findField(Sample.class, fieldName); + assertThat(field).isNotNull(); + return field; + } + + private Method sampleMethod(String noArgMethodName) { + Method method = ReflectionUtils.findMethod(Sample.class, noArgMethodName); + assertThat(method).isNotNull(); + return method; + } + + private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) { + TestBean annotation = field.getAnnotation(TestBean.class); + String beanName = (StringUtils.hasText(annotation.value()) ? annotation.value() : null); + return new TestBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), beanName, overrideMethod); + } + + static class SampleOneOverride { + + @TestBean(methodName = "message") + String message; + + static String message() { + return "OK"; + } + + } + + static class SampleOneOverrideWithName { + + @TestBean(name = "anotherBean", methodName = "message") + String message; + + static String message() { + return "OK"; + } + + } + + static class SampleMissingMethod { + + @TestBean + String message; + + } + + + @SuppressWarnings("unused") + static class Sample { + + @TestBean + private String message; + + @TestBean + private String message2; + + @TestBean(name = "anotherBean") + private String message3; + + @Qualifier("anotherBean") + @TestBean + private String message4; + + static String message() { + return "OK"; + } + + static String description() { + return message(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeIntegrationTests.java index 2aa512b24f9..28bd78c05a0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.test.context.bean.override.mockito; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -30,7 +31,7 @@ import org.springframework.test.context.bean.override.example.RealExampleService import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.times; @@ -80,9 +81,9 @@ public class MockitoBeanByTypeIntegrationTests { .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) .isSameAs(ctx.getBean("ambiguous2")); - assertThatException() + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) .isThrownBy(() -> ctx.getBean(StringBuilder.class)) - .withMessageEndingWith("but found 2: ambiguous2,ambiguous1"); + .satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2")); assertThat(this.ambiguous).isEmpty(); assertThat(this.ambiguous.substring(0)).isNull(); @@ -97,9 +98,9 @@ public class MockitoBeanByTypeIntegrationTests { .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) .isSameAs(ctx.getBean("ambiguous1")); - assertThatException() + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) .isThrownBy(() -> ctx.getBean(StringBuilder.class)) - .withMessageEndingWith("but found 2: ambiguous2,ambiguous1"); + .satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2")); assertThat(this.ambiguousMeta).isEmpty(); assertThat(this.ambiguousMeta.substring(0)).isNull(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java new file mode 100644 index 00000000000..61b1fd85031 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java @@ -0,0 +1,151 @@ +/* + * 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.io.Externalizable; +import java.lang.reflect.Field; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.Answers; + +import org.springframework.core.ResolvableType; +import org.springframework.test.context.bean.override.OverrideMetadata; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockitoBeanOverrideMetadata}. + * + * @author Stephane Nicoll + */ +class MockitoBeanOverrideMetadataTests { + + @Test + void forTestClassSetsNameToNullIfAnnotationNameIsNull() { + List list = OverrideMetadata.forTestClass(SampleOneMock.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isNull()); + } + + @Test + void forTestClassSetsNameToAnnotationName() { + List list = OverrideMetadata.forTestClass(SampleOneMockWithName.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isEqualTo("anotherService")); + } + + @Test + void isEqualToWithSameInstance() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + assertThat(metadata).isEqualTo(metadata); + assertThat(metadata).hasSameHashCodeAs(metadata); + } + + @Test + void isEqualToWithSameMetadata() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isEqualToWithSameMetadataButDifferentField() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service2")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentBeanName() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service3")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentExtraInterfaces() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentAnswers() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentSerializableFlag() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service6")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + + private Field sampleField(String fieldName) { + Field field = ReflectionUtils.findField(Sample.class, fieldName); + assertThat(field).isNotNull(); + return field; + } + + private MockitoBeanOverrideMetadata createMetadata(Field field) { + MockitoBean annotation = field.getAnnotation(MockitoBean.class); + return new MockitoBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), annotation); + } + + + static class SampleOneMock { + + @MockitoBean + String service; + + } + + static class SampleOneMockWithName { + + @MockitoBean(name = "anotherService") + String service; + + } + + static class Sample { + + @MockitoBean + private String service; + + @MockitoBean + private String service2; + + @MockitoBean(name = "beanToMock") + private String service3; + + @MockitoBean(extraInterfaces = Externalizable.class) + private String service4; + + @MockitoBean(answers = Answers.RETURNS_MOCKS) + private String service5; + + @MockitoBean(serializable = true) + private String service6; + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java new file mode 100644 index 00000000000..c02e4ebfa93 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java @@ -0,0 +1,139 @@ +/* + * 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.reflect.Field; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.test.context.bean.override.OverrideMetadata; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockitoSpyBeanOverrideMetadata}. + * + * @author Stephane Nicoll + */ +class MockitoSpyBeanOverrideMetadataTests { + + @Test + void forTestClassSetsNameToNullIfAnnotationNameIsNull() { + List list = OverrideMetadata.forTestClass(SampleOneSpy.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isNull()); + } + + @Test + void forTestClassSetsNameToAnnotationName() { + List list = OverrideMetadata.forTestClass(SampleOneSpyWithName.class); + assertThat(list).singleElement().satisfies(metadata -> assertThat(metadata.getBeanName()).isEqualTo("anotherService")); + } + + @Test + void isEqualToWithSameInstance() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + assertThat(metadata).isEqualTo(metadata); + assertThat(metadata).hasSameHashCodeAs(metadata); + } + + @Test + void isEqualToWithSameMetadata() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isEqualToWithSameMetadataButDifferentField() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service2")); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentBeanName() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service3")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentReset() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isNotEqualToWithSameMetadataButDifferentProxyTargetAwareFlag() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + + private Field sampleField(String fieldName) { + Field field = ReflectionUtils.findField(Sample.class, fieldName); + assertThat(field).isNotNull(); + return field; + } + + private MockitoSpyBeanOverrideMetadata createMetadata(Field field) { + MockitoSpyBean annotation = field.getAnnotation(MockitoSpyBean.class); + return new MockitoSpyBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), annotation); + } + + + static class SampleOneSpy { + + @MockitoSpyBean + String service; + + } + + static class SampleOneSpyWithName { + + @MockitoSpyBean(name = "anotherService") + String service; + + } + + static class Sample { + + @MockitoSpyBean + private String service; + + @MockitoSpyBean + private String service2; + + @MockitoSpyBean(name = "beanToMock") + private String service3; + + @MockitoSpyBean(reset = MockReset.BEFORE) + private String service4; + + @MockitoSpyBean(proxyTargetAware = false) + private String service5; + + } + +}