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; + + } + +}