From 6f5d3a4b12719dfced66582f46b6887e7763b796 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 10 Mar 2024 16:05:55 +0100 Subject: [PATCH] Polish Bean Override support in the TestContext framework --- .../test/bean/override/BeanOverride.java | 21 +-- .../BeanOverrideBeanPostProcessor.java | 128 ++++++++++-------- .../BeanOverrideContextCustomizerFactory.java | 11 +- .../bean/override/BeanOverrideParser.java | 61 +++++---- .../bean/override/BeanOverrideProcessor.java | 38 +++--- .../bean/override/BeanOverrideStrategy.java | 18 +-- .../BeanOverrideTestExecutionListener.java | 46 ++++--- .../test/bean/override/OverrideMetadata.java | 42 +++--- .../bean/override/mockito/Definition.java | 9 +- .../bean/override/mockito/MockDefinition.java | 29 ++-- .../test/bean/override/mockito/MockReset.java | 3 +- .../bean/override/mockito/MockitoBean.java | 29 ++-- .../mockito/MockitoBeanOverrideProcessor.java | 3 +- .../bean/override/mockito/MockitoBeans.java | 4 +- .../MockitoResetTestExecutionListener.java | 3 +- .../bean/override/mockito/MockitoSpyBean.java | 19 +-- .../mockito/MockitoTestExecutionListener.java | 27 ++-- .../bean/override/mockito/SpyDefinition.java | 40 +++--- .../BeanOverrideBeanPostProcessorTests.java | 29 ++-- .../override/BeanOverrideParserTests.java | 58 ++++---- .../example/ExampleBeanOverrideProcessor.java | 5 +- 21 files changed, 332 insertions(+), 291 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java index 114f85769500..9872885ec5ed 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java @@ -23,24 +23,27 @@ /** * Mark an annotation as eligible for Bean Override parsing. - * This meta-annotation provides a {@link BeanOverrideProcessor} class which - * must be capable of handling the annotated annotation. * - *

Target annotation must have a {@link RetentionPolicy} of {@code RUNTIME} - * and be applicable to {@link java.lang.reflect.Field Fields} only. - * @see BeanOverrideBeanPostProcessor + *

This meta-annotation specifies a {@link BeanOverrideProcessor} class which + * must be capable of handling the composed annotation that is meta-annotated + * with {@code @BeanOverride}. + * + *

The composed annotation that is meta-annotated with {@code @BeanOverride} + * must have a {@code RetentionPolicy} of {@link RetentionPolicy#RUNTIME RUNTIME} + * and a {@code Target} of {@link ElementType#FIELD FIELD}. * * @author Simon Baslé * @since 6.2 + * @see BeanOverrideBeanPostProcessor */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.ANNOTATION_TYPE}) +@Target(ElementType.ANNOTATION_TYPE) public @interface BeanOverride { /** - * A {@link BeanOverrideProcessor} implementation class by which the target - * annotation should be processed. Implementations must have a no-argument - * constructor. + * A {@link BeanOverrideProcessor} implementation class by which the composed + * annotation should be processed. */ Class value(); + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java index e6561e1bba3c..d6433788bc82 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java @@ -53,19 +53,20 @@ /** * A {@link BeanFactoryPostProcessor} used to register and inject overriding - * bean metadata with the {@link ApplicationContext}. A set of - * {@link OverrideMetadata} must be passed to the processor. - * A {@link BeanOverrideParser} can typically be used to parse these from test - * classes that use any annotation meta-annotated with {@link BeanOverride} to - * mark override sites. + * bean metadata with the {@link ApplicationContext}. * - *

This processor supports two {@link BeanOverrideStrategy}: + *

A set of {@link OverrideMetadata} must be provided to this processor. A + * {@link BeanOverrideParser} can typically be used to parse this metadata from + * test classes that use any annotation meta-annotated with + * {@link BeanOverride @BeanOverride} to mark override sites. + * + *

This processor supports two types of {@link BeanOverrideStrategy}: *

* *

This processor also provides support for injecting the overridden bean @@ -78,19 +79,25 @@ public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPost BeanFactoryAware, BeanFactoryPostProcessor, Ordered { private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName(); - private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); - private final Set overrideMetadata; - private final Map earlyOverrideMetadata = new HashMap<>(); + private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = + BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); - private ConfigurableListableBeanFactory beanFactory; + + private final Map earlyOverrideMetadata = new HashMap<>(); private final Map beanNameRegistry = new HashMap<>(); private final Map fieldRegistry = new HashMap<>(); + private final Set overrideMetadata; + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + /** - * Create a new {@link BeanOverrideBeanPostProcessor} instance with the + * Create a new {@code BeanOverrideBeanPostProcessor} instance with the * given {@link OverrideMetadata} set. * @param overrideMetadata the initial override metadata */ @@ -107,7 +114,7 @@ public int getOrder() { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, - "Beans overriding can only be used with a ConfigurableListableBeanFactory"); + "Bean overriding can only be used with a ConfigurableListableBeanFactory"); this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } @@ -120,7 +127,7 @@ protected Set getOverrideMetadata() { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - Assert.state(this.beanFactory == beanFactory, "Unexpected beanFactory to postProcess"); + Assert.state(this.beanFactory == beanFactory, "Unexpected BeanFactory to post-process"); Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory, "Bean overriding annotations can only be used on bean factories that implement " + "BeanDefinitionRegistry"); @@ -128,17 +135,17 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } private void postProcessWithRegistry(BeanDefinitionRegistry registry) { - //Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed - Set overrideMetadata = getOverrideMetadata(); - for (OverrideMetadata metadata : overrideMetadata) { + // Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed. + for (OverrideMetadata metadata : getOverrideMetadata()) { registerBeanOverride(registry, metadata); } } /** - * Copy the details of a {@link BeanDefinition} to the definition created by - * this processor for a given {@link OverrideMetadata}. Defaults to copying - * the {@link BeanDefinition#isPrimary()} attribute and scope. + * Copy certain details of a {@link BeanDefinition} to the definition created by + * this processor for a given {@link OverrideMetadata}. + *

The default implementation copies the {@linkplain BeanDefinition#isPrimary() + * primary flag} and the {@linkplain BeanDefinition#getScope() scope}. */ protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { to.setPrimary(from.isPrimary()); @@ -155,6 +162,7 @@ private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetad private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); String beanName = overrideMetadata.getExpectedBeanName(); @@ -166,7 +174,7 @@ private void registerReplaceDefinition(BeanDefinitionRegistry registry, Override } else if (enforceExistingDefinition) { throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() + - " bean, expected a bean definition to replace with name '" + beanName + "'"); + " bean; expected a bean definition to replace with name '" + beanName + "'"); } registry.registerBeanDefinition(beanName, beanDefinition); @@ -185,10 +193,10 @@ else if (enforceExistingDefinition) { /** * Check that the expected bean name is registered and matches the type to override. - * If so, put the override metadata in the early tracking map. - * The map will later be checked to see if a given bean should be wrapped + *

If so, put the override metadata in the early tracking map. + *

The map will later be checked to see if a given bean should be wrapped * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} - * phase + * phase. */ private void registerWrapEarly(OverrideMetadata metadata) { Set existingBeanNames = getExistingBeanNames(metadata.typeToOverride()); @@ -203,11 +211,12 @@ private void registerWrapEarly(OverrideMetadata metadata) { } /** - * Check early overrides records and use the {@link OverrideMetadata} to + * Check early override records and use the {@link OverrideMetadata} to * create an override instance from the provided bean, if relevant. *

Called during the {@link SmartInstantiationAwareBeanPostProcessor} - * phases (see {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} - * and {@link WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String)}). + * phases. + * @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String) + * @see WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String) */ protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException { final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); @@ -236,17 +245,15 @@ private Set getExistingBeanNames(ResolvableType resolvableType) { beans.add(beanName); } } - beans.removeIf(this::isScopedTarget); + beans.removeIf(ScopedProxyUtils::isScopedTarget); return beans; } - private boolean isScopedTarget(String beanName) { - try { - return ScopedProxyUtils.isScopedTarget(beanName); - } - catch (Throwable ex) { - return false; - } + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + throws BeansException { + ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); + return pvs; } private void postProcessField(Object bean, Field field) { @@ -256,16 +263,10 @@ private void postProcessField(Object bean, Field field) { } } - @Override - public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) - throws BeansException { - ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); - return pvs; - } - void inject(Field field, Object target, OverrideMetadata overrideMetadata) { String beanName = this.beanNameRegistry.get(overrideMetadata); - Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for overrideMetadata " + overrideMetadata); + Assert.state(StringUtils.hasLength(beanName), + () -> "No bean found for OverrideMetadata: " + overrideMetadata); inject(field, target, beanName); } @@ -287,25 +288,26 @@ private void inject(Field field, Object target, String beanName) { } /** - * Register the processor with a {@link BeanDefinitionRegistry}. - * Not required when using the Spring TestContext Framework, as registration - * is automatic via the {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * Register a {@link BeanOverrideBeanPostProcessor} with a {@link BeanDefinitionRegistry}. + *

Not required when using the Spring TestContext Framework, as registration + * is automatic via the + * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} * mechanism. * @param registry the bean definition registry * @param overrideMetadata the initial override metadata set */ public static void register(BeanDefinitionRegistry registry, @Nullable Set overrideMetadata) { - //early processor - getOrAddInfrastructureBeanDefinition(registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, - constructorArguments -> constructorArguments.addIndexedArgumentValue(0, - new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); - - //main processor - BeanDefinition definition = getOrAddInfrastructureBeanDefinition(registry, BeanOverrideBeanPostProcessor.class, - INFRASTRUCTURE_BEAN_NAME, constructorArguments -> constructorArguments - .addIndexedArgumentValue(0, new LinkedHashSet())); - ConstructorArgumentValues.ValueHolder constructorArg = definition.getConstructorArgumentValues() - .getIndexedArgumentValue(0, Set.class); + // Early processor + getOrAddInfrastructureBeanDefinition( + registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs -> + constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); + + // Main processor + BeanDefinition definition = getOrAddInfrastructureBeanDefinition( + registry, BeanOverrideBeanPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs -> + constructorArgs.addIndexedArgumentValue(0, new LinkedHashSet())); + ConstructorArgumentValues.ValueHolder constructorArg = + definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class); @SuppressWarnings("unchecked") Set existing = (Set) constructorArg.getValue(); if (overrideMetadata != null && existing != null) { @@ -315,6 +317,7 @@ public static void register(BeanDefinitionRegistry registry, @Nullable Set clazz, String beanName, Consumer constructorArgumentsConsumer) { + if (!registry.containsBeanDefinition(beanName)) { RootBeanDefinition definition = new RootBeanDefinition(clazz); definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); @@ -326,17 +329,21 @@ private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitio return registry.getBeanDefinition(beanName); } + private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, PriorityOrdered { private final BeanOverrideBeanPostProcessor mainProcessor; + private final Map earlyReferences; + private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) { this.mainProcessor = mainProcessor; this.earlyReferences = new ConcurrentHashMap<>(16); } + @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; @@ -363,8 +370,9 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } private String getCacheKey(Object bean, String beanName) { - return StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName(); + return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName()); } } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java index cf394301d75a..d7a25db125df 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Set; -import org.springframework.aot.hint.annotation.Reflective; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfigurationAttributes; @@ -29,7 +28,8 @@ import org.springframework.test.context.TestContextAnnotationUtils; /** - * A {@link ContextCustomizerFactory} to add support for Bean Overriding. + * {@link ContextCustomizerFactory} which provides support for Bean Overriding + * in tests. * * @author Simon Baslé * @since 6.2 @@ -39,6 +39,7 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFa @Override public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { + BeanOverrideParser parser = new BeanOverrideParser(); parseMetadata(testClass, parser); if (parser.getOverrideMetadata().isEmpty()) { @@ -56,10 +57,9 @@ private void parseMetadata(Class testClass, BeanOverrideParser parser) { } /** - * A {@link ContextCustomizer} for Bean Overriding in tests. + * {@link ContextCustomizer} for Bean Overriding in tests. */ - @Reflective - static final class BeanOverrideContextCustomizer implements ContextCustomizer { + private static final class BeanOverrideContextCustomizer implements ContextCustomizer { private final Set metadata; @@ -97,4 +97,5 @@ public int hashCode() { return this.metadata.hashCode(); } } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java index c4d25b7c596e..1741cbf3d068 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java @@ -34,23 +34,22 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; + /** - * A parser that discovers annotations meta-annotated with {@link BeanOverride} + * A parser that discovers annotations meta-annotated with {@link BeanOverride @BeanOverride} * on fields of a given class and creates {@link OverrideMetadata} accordingly. * * @author Simon Baslé + * @since 6.2 */ class BeanOverrideParser { - private final Set parsedMetadata; + private final Set parsedMetadata = new LinkedHashSet<>(); - BeanOverrideParser() { - this.parsedMetadata = new LinkedHashSet<>(); - } /** - * Getter for the set of {@link OverrideMetadata} once {@link #parse(Class)} - * has been called. + * Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called. */ Set getOverrideMetadata() { return Collections.unmodifiableSet(this.parsedMetadata); @@ -58,12 +57,12 @@ Set getOverrideMetadata() { /** * Discover fields of the provided class that are meta-annotated with - * {@link BeanOverride}, then instantiate their corresponding - * {@link BeanOverrideProcessor} and use it to create an {@link OverrideMetadata} - * instance for each field. Each call to {@code parse} adds the parsed - * metadata to the parser's override metadata {{@link #getOverrideMetadata()} - * set} - * @param testClass the class which fields to inspect + * {@link BeanOverride @BeanOverride}, then instantiate the corresponding + * {@link BeanOverrideProcessor} and use it to create {@link OverrideMetadata} + * for each field. + *

Each call to {@code parse} adds the parsed metadata to the parser's + * override metadata {@link #getOverrideMetadata() set}. + * @param testClass the test class in which to inspect fields */ void parse(Class testClass) { ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass)); @@ -71,11 +70,12 @@ void parse(Class testClass) { /** * Check if any field of the provided {@code testClass} is meta-annotated - * with {@link BeanOverride}. + * with {@link BeanOverride @BeanOverride}. *

This is similar to the initial discovery of fields in {@link #parse(Class)} * without the heavier steps of instantiating processors and creating - * {@link OverrideMetadata}, so this method leaves the current state of - * {@link #getOverrideMetadata()} unchanged. + * {@link OverrideMetadata}. Consequently, this method leaves the current + * state of the parser's override metadata {@link #getOverrideMetadata() set} + * unchanged. * @param testClass the class which fields to inspect * @return true if there is a bean override annotation present, false otherwise * @see #parse(Class) @@ -86,7 +86,7 @@ boolean hasBeanOverride(Class testClass) { if (hasBeanOverride.get()) { return; } - final long count = MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + long count = MergedAnnotations.from(field, DIRECT) .stream(BeanOverride.class) .count(); hasBeanOverride.compareAndSet(false, count > 0L); @@ -97,45 +97,46 @@ boolean hasBeanOverride(Class testClass) { private void parseField(Field field, Class source) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + MergedAnnotations.from(field, DIRECT) .stream(BeanOverride.class) - .map(bo -> { - MergedAnnotation a = bo.getMetaSource(); - Assert.notNull(a, "BeanOverride annotation must be meta-present"); - return new AnnotationPair(a.synthesize(), bo); + .map(mergedAnnotation -> { + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + Assert.notNull(metaSource, "@BeanOverride annotation must be meta-present"); + return new AnnotationPair(metaSource.synthesize(), mergedAnnotation); }) .forEach(pair -> { - BeanOverride metaAnnotation = pair.metaAnnotation().synthesize(); - final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value()); + BeanOverride beanOverride = pair.mergedAnnotation().synthesize(); + BeanOverrideProcessor processor = getProcessorInstance(beanOverride.value()); if (processor == null) { return; } ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.annotation(), source); Assert.state(overrideAnnotationFound.compareAndSet(false, true), - "Multiple bean override annotations found on annotated field <" + field + ">"); + () -> "Multiple @BeanOverride annotations found on field: " + field); OverrideMetadata metadata = processor.createMetadata(field, pair.annotation(), typeToOverride); boolean isNewDefinition = this.parsedMetadata.add(metadata); Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + - " overrideMetadata " + metadata); + " OverrideMetadata: " + metadata); }); } @Nullable private BeanOverrideProcessor getProcessorInstance(Class processorClass) { - final Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); + Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); if (constructor != null) { - ReflectionUtils.makeAccessible(constructor); try { + ReflectionUtils.makeAccessible(constructor); return constructor.newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { - throw new BeanDefinitionValidationException("Could not get an instance of BeanOverrideProcessor", ex); + throw new BeanDefinitionValidationException( + "Failed to instantiate BeanOverrideProcessor of type " + processorClass.getName(), ex); } } return null; } - private record AnnotationPair(Annotation annotation, MergedAnnotation metaAnnotation) {} + private record AnnotationPair(Annotation annotation, MergedAnnotation mergedAnnotation) {} } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java index 3019754c3011..b3152ee2cb85 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java @@ -22,14 +22,13 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.MergedAnnotation; /** - * An interface for Bean Overriding concrete processing. + * Strategy interface for Bean Override processing. * *

Processors are generally linked to one or more specific concrete annotations - * (meta-annotated with {@link BeanOverride}) and specify different steps in the - * process of parsing these annotations, ultimately creating + * (meta-annotated with {@link BeanOverride @BeanOverride}) and specify different + * steps in the process of parsing these annotations, ultimately creating * {@link OverrideMetadata} which will be used to instantiate the overrides. * *

Implementations are required to have a no-argument constructor and be @@ -42,30 +41,31 @@ public interface BeanOverrideProcessor { /** - * Determine a {@link ResolvableType} for which an {@link OverrideMetadata} - * instance will be created, e.g. by using the annotation to determine the - * type. - *

Defaults to the field corresponding {@link ResolvableType}, - * additionally tracking the source class if the field is a {@link TypeVariable}. + * Determine the {@link ResolvableType} for which an {@link OverrideMetadata} + * instance will be created — for example, by using the supplied annotation + * to determine the type. + *

The default implementation deduces the field's corresponding + * {@link ResolvableType}, additionally tracking the source class if the + * field's type is a {@link TypeVariable}. */ default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class source) { - return (field.getGenericType() instanceof TypeVariable) ? ResolvableType.forField(field, source) - : ResolvableType.forField(field); + return (field.getGenericType() instanceof TypeVariable ? + ResolvableType.forField(field, source) : ResolvableType.forField(field)); } /** - * Create an {@link OverrideMetadata} for a given annotated field and target - * {@link #getOrDeduceType(Field, Annotation, Class) type}. - * Specific implementations of metadata can have state to be used during - * override {@link OverrideMetadata#createOverride(String, BeanDefinition, - * Object) instance creation}, that is from further parsing the annotation or - * the annotated field. + * Create an {@link OverrideMetadata} instance for the given annotated field + * and target {@link #getOrDeduceType(Field, Annotation, Class) type}. + *

Specific implementations of metadata can have state to be used during + * override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition, + * Object) instance creation} — for example, from further parsing of the + * annotation or the annotated field. * @param field the annotated field * @param overrideAnnotation the field annotation * @param typeToOverride the target type - * @return a new {@link OverrideMetadata} + * @return a new {@link OverrideMetadata} instance * @see #getOrDeduceType(Field, Annotation, Class) - * @see MergedAnnotation#synthesize() */ OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride); + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java index bb030b2a583b..2c51a475d38b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java @@ -17,7 +17,7 @@ package org.springframework.test.bean.override; /** - * Strategies for override instantiation, implemented in + * Strategies for bean override instantiation, implemented in * {@link BeanOverrideBeanPostProcessor}. * * @author Simon Baslé @@ -26,22 +26,22 @@ public enum BeanOverrideStrategy { /** - * Replace a given bean definition, immediately preparing a singleton - * instance. Enforces the original bean definition to exist. + * Replace a given bean definition, immediately preparing a singleton instance. + *

Requires that the original bean definition exists. */ REPLACE_DEFINITION, /** - * Replace a given bean definition, immediately preparing a singleton - * instance. If the original bean definition does not exist, create the - * override definition instead of failing. + * Replace a given bean definition, immediately preparing a singleton instance. + *

If the original bean definition does not exist, an override definition + * will be created instead of failing. */ REPLACE_OR_CREATE_DEFINITION, /** - * Intercept and wrap the actual bean instance upon creation, during - * {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) - * early bean definition}. + * Intercept and wrap the actual bean instance upon creation, during the {@linkplain + * org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) + * early bean reference} phase. */ WRAP_EARLY_BEAN diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java index a2b5a7f1d5d6..91dd9760b62e 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java @@ -20,17 +20,17 @@ import java.util.function.BiConsumer; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ReflectionUtils; /** - * A {@link TestExecutionListener} implementation that enables Bean Override - * support in tests, injecting overridden beans in appropriate fields. + * {@code TestExecutionListener} that enables Bean Override support in tests, + * injecting overridden beans in appropriate fields of the test instance. * *

Some flavors of Bean Override might additionally require the use of - * additional listeners, which should be mentioned in the annotation(s) javadoc. + * additional listeners, which should be mentioned in the javadoc for the + * corresponding annotations. * * @author Simon Baslé * @since 6.2 @@ -68,36 +68,42 @@ protected void injectFields(TestContext testContext) { /** * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata * associated with the current test class and ensure fields are nulled out - * then re-injected with the overridden bean instance. This method does - * nothing if the {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} - * attribute is not present in the {@code testContext}. + * and then re-injected with the overridden bean instance. + *

This method does nothing if the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * attribute is not present in the {@code TestContext}. */ protected void reinjectFieldsIfConfigured(final TestContext testContext) throws Exception { if (Boolean.TRUE.equals( testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { + postProcessFields(testContext, (testMetadata, postProcessor) -> { - Field f = testMetadata.overrideMetadata.field(); - ReflectionUtils.makeAccessible(f); - ReflectionUtils.setField(f, testMetadata.testInstance(), null); - postProcessor.inject(f, testMetadata.testInstance(), testMetadata.overrideMetadata()); + Object testInstance = testMetadata.testInstance; + Field field = testMetadata.overrideMetadata.field(); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, testInstance, null); + postProcessor.inject(field, testInstance, testMetadata.overrideMetadata); }); } } private void postProcessFields(TestContext testContext, BiConsumer consumer) { - //avoid full parsing but validate that this particular class has some bean override field(s) + + Class testClass = testContext.getTestClass(); + Object testInstance = testContext.getTestInstance(); BeanOverrideParser parser = new BeanOverrideParser(); - if (parser.hasBeanOverride(testContext.getTestClass())) { - BeanOverrideBeanPostProcessor postProcessor = testContext.getApplicationContext() - .getBean(BeanOverrideBeanPostProcessor.class); - // the class should have already been parsed by the context customizer - for (OverrideMetadata metadata: postProcessor.getOverrideMetadata()) { - if (!metadata.field().getDeclaringClass().equals(testContext.getTestClass())) { + + // Avoid full parsing, but validate that this particular class has some bean override field(s). + if (parser.hasBeanOverride(testClass)) { + BeanOverrideBeanPostProcessor postProcessor = + testContext.getApplicationContext().getBean(BeanOverrideBeanPostProcessor.class); + // The class should have already been parsed by the context customizer. + for (OverrideMetadata metadata : postProcessor.getOverrideMetadata()) { + if (!metadata.field().getDeclaringClass().equals(testClass)) { continue; } - consumer.accept(new TestContextOverrideMetadata(testContext.getTestInstance(), metadata), - postProcessor); + consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), postProcessor); } } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java index d76f550adf40..ff2e693a5c17 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; /** @@ -41,24 +42,27 @@ public abstract class OverrideMetadata { private final BeanOverrideStrategy strategy; + protected OverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + this.field = field; this.overrideAnnotation = overrideAnnotation; this.typeToOverride = typeToOverride; this.strategy = strategy; } + /** - * Return a short human-readable description of the kind of override this + * Return a short, human-readable description of the kind of override this * instance handles. */ public abstract String getBeanOverrideDescription(); /** - * Return the expected bean name to override. Typically, this is either - * explicitly set in the concrete annotations or defined by the annotated - * field's name. + * Return the expected bean name to override. + *

Typically, this is either explicitly set in a concrete annotation or + * inferred from the annotated field's name. * @return the expected bean name */ protected String getExpectedBeanName() { @@ -74,7 +78,7 @@ public Field field() { /** * Return the concrete override annotation, that is the one meta-annotated - * with {@link BeanOverride}. + * with {@link BeanOverride @BeanOverride}. */ public Annotation overrideAnnotation() { return this.overrideAnnotation; @@ -103,21 +107,21 @@ public final BeanOverrideStrategy getBeanOverrideStrategy() { * @param existingBeanDefinition an existing bean definition for that bean * name, or {@code null} if not relevant * @param existingBeanInstance an existing instance for that bean name, - * for wrapping purpose, or {@code null} if irrelevant + * for wrapping purposes, or {@code null} if irrelevant * @return the instance with which to override the bean */ protected abstract Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance); /** - * Optionally track objects created by this {@link OverrideMetadata} - * (default is no tracking). + * Optionally track objects created by this {@link OverrideMetadata}. + *

The default is not to track, but this can be overridden in subclasses. * @param override the bean override instance to track - * @param trackingBeanRegistry the registry in which trackers could + * @param trackingBeanRegistry the registry in which trackers can * optionally be registered */ protected void track(Object override, SingletonBeanRegistry trackingBeanRegistry) { - //NO-OP + // NO-OP } @Override @@ -132,21 +136,23 @@ public boolean equals(Object obj) { return Objects.equals(this.field, that.field) && Objects.equals(this.overrideAnnotation, that.overrideAnnotation) && Objects.equals(this.strategy, that.strategy) && - Objects.equals(this.typeToOverride, that.typeToOverride); + Objects.equals(typeToOverride(), that.typeToOverride()); } @Override public int hashCode() { - return Objects.hash(this.field, this.overrideAnnotation, this.strategy, this.typeToOverride); + return Objects.hash(this.field, this.overrideAnnotation, this.strategy, typeToOverride()); } @Override public String toString() { - return "OverrideMetadata[" + - "category=" + this.getBeanOverrideDescription() + ", " + - "field=" + this.field + ", " + - "overrideAnnotation=" + this.overrideAnnotation + ", " + - "strategy=" + this.strategy + ", " + - "typeToOverride=" + this.typeToOverride + ']'; + return new ToStringCreator(this) + .append("category", getBeanOverrideDescription()) + .append("field", this.field) + .append("overrideAnnotation", this.overrideAnnotation) + .append("strategy", this.strategy) + .append("typeToOverride", typeToOverride()) + .toString(); } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java index 57ad9c26e837..42bc1c3885e1 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-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. @@ -32,10 +32,12 @@ * Base class for {@link MockDefinition} and {@link SpyDefinition}. * * @author Phillip Webb + * @since 6.2 */ abstract class Definition extends OverrideMetadata { - static final int MULTIPLIER = 31; + protected static final int MULTIPLIER = 31; + protected final String name; @@ -43,14 +45,17 @@ abstract class Definition extends OverrideMetadata { private final boolean proxyTargetAware; + Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + super(field, annotation, typeToOverride, strategy); this.name = name; this.reset = (reset != null) ? reset : MockReset.AFTER; this.proxyTargetAware = proxyTargetAware; } + @Override protected String getExpectedBeanName() { if (StringUtils.hasText(this.name)) { diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java index 05fea8394959..19f03b51540b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -42,17 +42,17 @@ * A complete definition that can be used to create a Mockito mock. * * @author Phillip Webb + * @since 6.2 */ class MockDefinition extends Definition { - private static final int MULTIPLIER = 31; - private final Set> extraInterfaces; private final Answers answer; private final boolean serializable; + MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) { this(annotation.name(), annotation.reset(), field, annotation, typeToMock, annotation.extraInterfaces(), annotation.answers(), annotation.serializable()); @@ -60,6 +60,7 @@ class MockDefinition extends Definition { MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock, Class[] extraInterfaces, @Nullable Answers answer, boolean serializable) { + super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION); Assert.notNull(typeToMock, "TypeToMock must not be null"); this.extraInterfaces = asClassSet(extraInterfaces); @@ -67,6 +68,7 @@ class MockDefinition extends Definition { this.serializable = serializable; } + @Override public String getBeanOverrideDescription() { return "mock"; @@ -119,7 +121,7 @@ public boolean equals(@Nullable Object obj) { } MockDefinition other = (MockDefinition) obj; boolean result = super.equals(obj); - result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); + result = result && ObjectUtils.nullSafeEquals(typeToOverride(), other.typeToOverride()); result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces); result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer); result = result && this.serializable == other.serializable; @@ -129,7 +131,7 @@ public boolean equals(@Nullable Object obj) { @Override public int hashCode() { int result = super.hashCode(); - result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride()); result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces); result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer); result = MULTIPLIER * result + Boolean.hashCode(this.serializable); @@ -138,13 +140,14 @@ public int hashCode() { @Override public String toString() { - return new ToStringCreator(this).append("name", this.name) - .append("typeToMock", this.typeToOverride()) - .append("extraInterfaces", this.extraInterfaces) - .append("answer", this.answer) - .append("serializable", this.serializable) - .append("reset", getReset()) - .toString(); + return new ToStringCreator(this) + .append("name", this.name) + .append("typeToMock", typeToOverride()) + .append("extraInterfaces", this.extraInterfaces) + .append("answer", this.answer) + .append("serializable", this.serializable) + .append("reset", getReset()) + .toString(); } T createMock() { @@ -164,7 +167,7 @@ T createMock(String name) { if (this.serializable) { settings.serializable(); } - return (T) mock(this.typeToOverride().resolve(), settings); + return (T) mock(typeToOverride().resolve(), settings); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java index e5d54c79ebe8..51b2dbed90bd 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -53,6 +53,7 @@ public enum MockReset { */ NONE; + /** * Create {@link MockSettings settings} to be used with mocks where reset * should occur before each test method runs. diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java index 83a50faf7608..c95ed9dba92f 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java @@ -34,12 +34,14 @@ * a new one will be added to the context. * *

Dependencies that are known to the application context but are not beans - * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) - * registered directly}) will not be found and a mocked bean will be added to + * (such as those + * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * registered directly}) will not be found, and a mocked bean will be added to * the context alongside the existing dependency. * * @author Simon Baslé * @since 6.2 + * @see MockitoSpyBean */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -48,35 +50,38 @@ public @interface MockitoBean { /** - * The name of the bean to register or replace. If not specified, it will be - * the name of the annotated field. - * @return the name of the bean + * The name of the bean to register or replace. + *

If not specified, the name of the annotated field will be used. + * @return the name of the mocked bean */ String name() default ""; /** - * Any extra interfaces that should also be declared on the mock. - * See {@link MockSettings#extraInterfaces(Class...)} for details. + * Extra interfaces that should also be declared on the mock. + *

Defaults to none. * @return any extra interfaces + * @see MockSettings#extraInterfaces(Class...) */ Class[] extraInterfaces() default {}; /** * The {@link Answers} type to use on the mock. + *

Defaults to {@link Answers#RETURNS_DEFAULTS}. * @return the answer type */ Answers answers() default Answers.RETURNS_DEFAULTS; /** - * If the generated mock is serializable. - * See {@link MockSettings#serializable()} for details. - * @return if the mock is serializable + * Whether the generated mock is serializable. + *

Defaults to {@code false}. + * @return {@code true} if the mock is serializable + * @see MockSettings#serializable() */ boolean serializable() default false; /** - * The reset mode to apply to the mock bean. - * The default is {@link MockReset#AFTER} meaning that mocks are + * The reset mode to apply to the mock. + *

The default is {@link MockReset#AFTER} meaning that mocks are * automatically reset after each test method is invoked. * @return the reset mode */ diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java index e82a24b7e96f..83ab8c8499d1 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -40,7 +40,8 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { return new SpyDefinition(spyBean, field, typeToMock); } - throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + overrideAnnotation.getClass().getName()); + throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + + overrideAnnotation.getClass().getName()); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java index 9431b4e872dc..7f596b6d7683 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-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. @@ -24,11 +24,13 @@ * Beans created using Mockito. * * @author Andy Wilkinson + * @since 6.2 */ class MockitoBeans implements Iterable { private final List beans = new ArrayList<>(); + void add(Object bean) { this.beans.add(bean); } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java index 04c91ebe6281..8eb6d55055a2 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -32,11 +32,10 @@ import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; /** - * {@link TestExecutionListener} to reset any mock beans that have been marked + * {@code TestExecutionListener} that resets any mock beans that have been marked * with a {@link MockReset}. * * @author Phillip Webb diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java index 030e9585a08a..d70e182cf9ad 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java @@ -34,11 +34,13 @@ * in the context. * *

Dependencies that are known to the application context but are not beans - * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * (such as those + * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly}) will not be found. * * @author Simon Baslé * @since 6.2 + * @see MockitoBean */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -47,16 +49,16 @@ public @interface MockitoSpyBean { /** - * The name of the bean to spy. If not specified, it will be the name of the - * annotated field. + * The name of the bean to spy. + *

If not specified, the name of the annotated field will be used. * @return the name of the spied bean */ String name() default ""; /** - * The reset mode to apply to the spied bean. The default is - * {@link MockReset#AFTER} meaning that spies are automatically reset after - * each test method is invoked. + * The reset mode to apply to the spied bean. + *

The default is {@link MockReset#AFTER} meaning that spies are automatically + * reset after each test method is invoked. * @return the reset mode */ MockReset reset() default MockReset.AFTER; @@ -65,10 +67,11 @@ * Indicates that Mockito methods such as {@link Mockito#verify(Object) * verify(mock)} should use the {@code target} of AOP advised beans, * rather than the proxy itself. - * If set to {@code false} you may need to use the result of + *

Defaults to {@code true}. + *

If set to {@code false} you may need to use the result of * {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object) * AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods. - * @return {@code true} if the target of AOP advised beans is used or + * @return {@code true} if the target of AOP advised beans is used, or * {@code false} if the proxy is used directly */ boolean proxyTargetAware() default true; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java index 245613ae812c..7d117a37e4f2 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java @@ -25,7 +25,6 @@ import org.mockito.MockitoAnnotations; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ClassUtils; @@ -33,27 +32,28 @@ import org.springframework.util.ReflectionUtils.FieldCallback; /** - * {@link TestExecutionListener} to enable {@link MockitoBean @MockitoBean} and + * {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} and * {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are - * used, primarily to allow {@link Captor @Captor} annotations. - *

- * The automatic reset support of {@code @MockBean} and {@code @SpyBean} is - * handled by sibling {@link MockitoResetTestExecutionListener}. + * used, primarily to support {@link Captor @Captor} annotations. + * + *

The automatic reset support for {@code @MockBean} and {@code @SpyBean} is + * handled by the {@link MockitoResetTestExecutionListener}. * * @author Simon Baslé * @author Phillip Webb * @author Andy Wilkinson * @author Moritz Halbritter - * @since 1.4.2 + * @since 6.2 * @see MockitoResetTestExecutionListener */ public class MockitoTestExecutionListener extends AbstractTestExecutionListener { + private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; + static final boolean mockitoPresent = ClassUtils.isPresent("org.mockito.MockSettings", MockitoTestExecutionListener.class.getClassLoader()); - private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; /** * Executes before {@link DependencyInjectionTestExecutionListener}. @@ -109,22 +109,23 @@ private void closeMocks(TestContext testContext) throws Exception { } private boolean hasMockitoAnnotations(TestContext testContext) { - MockitoAnnotationCollection collector = new MockitoAnnotationCollection(); + MockitoAnnotationCollector collector = new MockitoAnnotationCollector(); ReflectionUtils.doWithFields(testContext.getTestClass(), collector); return collector.hasAnnotations(); } + /** - * {@link FieldCallback} to collect Mockito annotations. + * {@link FieldCallback} that collects Mockito annotations. */ - private static final class MockitoAnnotationCollection implements FieldCallback { + private static final class MockitoAnnotationCollector implements FieldCallback { private final Set annotations = new LinkedHashSet<>(); @Override public void doWith(Field field) throws IllegalArgumentException { - for (Annotation annotation : field.getDeclaredAnnotations()) { - if (annotation.annotationType().getName().startsWith("org.mockito")) { + for (Annotation annotation : field.getAnnotations()) { + if (annotation.annotationType().getPackageName().startsWith("org.mockito")) { this.annotations.add(annotation); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java index 60775654bfd3..db21bc9c6793 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -19,7 +19,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Proxy; -import java.util.Objects; import org.mockito.AdditionalAnswers; import org.mockito.MockSettings; @@ -43,6 +42,7 @@ * A complete definition that can be used to create a Mockito spy. * * @author Phillip Webb + * @since 6.2 */ class SpyDefinition extends Definition { @@ -53,49 +53,53 @@ class SpyDefinition extends Definition { SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation, ResolvableType typeToSpy) { + super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN); Assert.notNull(typeToSpy, "typeToSpy must not be null"); } + @Override public String getBeanOverrideDescription() { return "spy"; } @Override - protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { - return createSpy(beanName, Objects.requireNonNull(existingBeanInstance, - "MockitoSpyBean requires an existing bean instance for bean " + beanName)); + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + + Assert.notNull(existingBeanInstance, + () -> "MockitoSpyBean requires an existing bean instance for bean " + beanName); + return createSpy(beanName, existingBeanInstance); } @Override public boolean equals(@Nullable Object obj) { - //for SpyBean we want the class to be exactly the same + // For SpyBean we want the class to be exactly the same. if (obj == this) { return true; } if (obj == null || obj.getClass() != getClass()) { return false; } - SpyDefinition other = (SpyDefinition) obj; - boolean result = super.equals(obj); - result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); - return result; + SpyDefinition that = (SpyDefinition) obj; + return (super.equals(obj) && ObjectUtils.nullSafeEquals(typeToOverride(), that.typeToOverride())); } @Override public int hashCode() { int result = super.hashCode(); - result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride()); return result; } @Override public String toString() { - return new ToStringCreator(this).append("name", this.name) - .append("typeToSpy", typeToOverride()) - .append("reset", getReset()) - .toString(); + return new ToStringCreator(this) + .append("name", this.name) + .append("typeToSpy", typeToOverride()) + .append("reset", getReset()) + .toString(); } T createSpy(Object instance) { @@ -105,7 +109,9 @@ T createSpy(Object instance) { @SuppressWarnings("unchecked") T createSpy(String name, Object instance) { Assert.notNull(instance, "Instance must not be null"); - Assert.isInstanceOf(Objects.requireNonNull(this.typeToOverride().resolve()), instance); + Class resolvedTypeToOverride = typeToOverride().resolve(); + Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override"); + Assert.isInstanceOf(resolvedTypeToOverride, instance); if (Mockito.mockingDetails(instance).isSpy()) { return (T) instance; } @@ -119,7 +125,7 @@ T createSpy(String name, Object instance) { Class toSpy; if (Proxy.isProxyClass(instance.getClass())) { settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance)); - toSpy = this.typeToOverride().toClass(); + toSpy = typeToOverride().toClass(); } else { settings.defaultAnswer(Mockito.CALLS_REAL_METHODS); diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java index 0a75cc9abf53..138bc5eee693 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.function.Predicate; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.BeanWrapper; @@ -47,18 +46,14 @@ import static org.assertj.core.api.Assertions.assertThatNoException; /** - * Test for {@link BeanOverrideBeanPostProcessor}. + * Tests for for {@link BeanOverrideBeanPostProcessor}. * * @author Simon Baslé */ class BeanOverrideBeanPostProcessorTests { - BeanOverrideParser parser; + private final BeanOverrideParser parser = new BeanOverrideParser(); - @BeforeEach - void initParser() { - this.parser = new BeanOverrideParser(); - } @Test void canReplaceExistingBeanDefinitions() { @@ -83,8 +78,9 @@ void cannotReplaceIfNoBeanMatching() { context.register(ReplaceBeans.class); //note we don't register any original bean here - assertThatIllegalStateException().isThrownBy(context::refresh).withMessage("Unable to override test bean, " + - "expected a bean definition to replace with name 'explicit'"); + assertThatIllegalStateException() + .isThrownBy(context::refresh) + .withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'"); } @Test @@ -125,7 +121,9 @@ void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class); context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); } @@ -139,11 +137,12 @@ void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType); context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); } - @Test void postProcessorShouldNotTriggerEarlyInitialization() { this.parser.parse(EagerInitBean.class); @@ -192,6 +191,7 @@ void copyDefinitionPrimaryAndScope() { .matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype"); } + /* Classes to parse and register with the bean post processor ----- @@ -228,7 +228,6 @@ static class CreateIfOriginalIsMissingBean { static ExampleService useThis() { return OVERRIDE_SERVICE; } - } @Configuration(proxyBeanMethods = false) @@ -245,7 +244,6 @@ static SomeInterface fOverride() { TestFactoryBean testFactoryBean() { return new TestFactoryBean(); } - } static class EagerInitBean { @@ -256,7 +254,6 @@ static class EagerInitBean { static ExampleService useThis() { return OVERRIDE_SERVICE; } - } static class SingletonBean { @@ -268,7 +265,6 @@ static class SingletonBean { static String useThis() { return "USED THIS"; } - } static class TestFactoryBean implements FactoryBean { @@ -287,7 +283,6 @@ public Class getObjectType() { public boolean isSingleton() { return true; } - } static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered { @@ -302,7 +297,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } - } static class EarlyBeanInitializationDetector implements BeanFactoryPostProcessor { @@ -314,15 +308,12 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) "factoryBeanInstanceCache"); Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered."); } - } interface SomeInterface { - } static class SomeImplementation implements SomeInterface { - } } diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java index ffa913510b0f..35a5640bd481 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java @@ -16,58 +16,63 @@ package org.springframework.test.bean.override; +import java.lang.reflect.Field; + import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.Configuration; import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; import org.springframework.test.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; import static org.springframework.test.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; +/** + * Unit tests for {@link BeanOverrideParser}. + * + * @since 6.2 + */ class BeanOverrideParserTests { + private final BeanOverrideParser parser = new BeanOverrideParser(); + + @Test void findsOnField() { - BeanOverrideParser parser = new BeanOverrideParser(); - parser.parse(OnFieldConf.class); + parser.parse(SingleAnnotationOnField.class); - assertThat(parser.getOverrideMetadata()).hasSize(1) - .first() - .extracting(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) - .isEqualTo("onField"); + assertThat(parser.getOverrideMetadata()) + .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) + .containsExactly("onField"); } @Test - void allowMultipleProcessorsOnDifferentElements() { - BeanOverrideParser parser = new BeanOverrideParser(); - parser.parse(MultipleFieldsWithOnFieldConf.class); + void allowsMultipleProcessorsOnDifferentElements() { + parser.parse(AnnotationsOnMultipleFields.class); assertThat(parser.getOverrideMetadata()) - .hasSize(2) .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) - .containsOnly("onField1", "onField2"); + .containsExactlyInAnyOrder("onField1", "onField2"); } @Test void rejectsMultipleAnnotationsOnSameElement() { - BeanOverrideParser parser = new BeanOverrideParser(); - assertThatRuntimeException().isThrownBy(() -> parser.parse(MultipleOnFieldConf.class)) - .withMessage("Multiple bean override annotations found on annotated field <" + - String.class.getName() + " " + MultipleOnFieldConf.class.getName() + ".message>"); + Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message"); + assertThatRuntimeException() + .isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class)) + .withMessage("Multiple @BeanOverride annotations found on field: " + field); } @Test void detectsDuplicateMetadata() { - BeanOverrideParser parser = new BeanOverrideParser(); - assertThatRuntimeException().isThrownBy(() -> parser.parse(DuplicateConf.class)) - .withMessage("Duplicate test overrideMetadata {DUPLICATE_TRIGGER}"); + assertThatRuntimeException() + .isThrownBy(() -> parser.parse(DuplicateConf.class)) + .withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}"); } - @Configuration - static class OnFieldConf { + static class SingleAnnotationOnField { @ExampleBeanOverrideAnnotation("onField") String message; @@ -75,11 +80,9 @@ static class OnFieldConf { static String onField() { return "OK"; } - } - @Configuration - static class MultipleOnFieldConf { + static class MultipleAnnotationsOnField { @ExampleBeanOverrideAnnotation("foo") @TestBeanOverrideMetaAnnotation @@ -88,11 +91,10 @@ static class MultipleOnFieldConf { static String foo() { return "foo"; } - } - @Configuration - static class MultipleFieldsWithOnFieldConf { + static class AnnotationsOnMultipleFields { + @ExampleBeanOverrideAnnotation("onField1") String message; @@ -108,7 +110,6 @@ static String onField2() { } } - @Configuration static class DuplicateConf { @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) @@ -116,7 +117,6 @@ static class DuplicateConf { @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) String message2; - } } diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java index 6df216f4fe31..b0566875cfd5 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java @@ -25,15 +25,13 @@ public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { - public ExampleBeanOverrideProcessor() { - } - private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() { @Override public String toString() { return "{DUPLICATE_TRIGGER}"; } }; + public static final String DUPLICATE_TRIGGER = "CONSTANT"; @Override @@ -46,4 +44,5 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio } return new TestOverrideMetadata(field, annotation, typeToOverride); } + }