Support @MockitoBean at the type level on test classes
Prior to this commit, @MockitoBean could only be declared on fields within test classes, which prevented developers from being able to easily reuse mock configuration across a test suite. With this commit, @MockitoBean is now supported at the type level on test classes, their superclasses, and interfaces implemented by those classes. @MockitoBean is also supported on enclosing classes for @Nested test classes, their superclasses, and interfaces implemented by those classes, while honoring @NestedTestConfiguration semantics. In addition, @MockitoBean: - has a new `types` attribute that can be used to declare the type or types to mock when @MockitoBean is declared at the type level - can be declared as a repeatable annotation at the type level - can be declared as a meta-annotation on a custom composed annotation which can be reused across a test suite (see the @SharedMocks example in the reference manual) To support these new features, this commit also includes the following changes. - The `field` property in BeanOverrideHandler is now @Nullable. - BeanOverrideProcessor has a new `default` createHandlers() method which is invoked when a @BeanOverride annotation is found at the type level. - MockitoBeanOverrideProcessor implements the new createHandlers() method. - The internal findHandlers() method in BeanOverrideHandler has been completely overhauled. - The @MockitoBean and @MockitoSpyBean section of the reference manual has been completely overhauled. Closes gh-33925
This commit is contained in:
@@ -25,15 +25,19 @@ import java.lang.annotation.Target;
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
|
||||
/**
|
||||
* Mark a composed annotation as eligible for Bean Override processing.
|
||||
* Mark a <em>composed annotation</em> as eligible for Bean Override processing.
|
||||
*
|
||||
* <p>Specifying this annotation registers the configured {@link BeanOverrideProcessor}
|
||||
* which must be capable of handling the composed annotation and its attributes.
|
||||
*
|
||||
* <p>Since the composed annotation should only be applied to non-static fields, it is
|
||||
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
|
||||
* <p>Since the composed annotation will typically only be applied to non-static
|
||||
* fields, it is expected that the composed annotation is meta-annotated with
|
||||
* {@link Target @Target(ElementType.FIELD)}. However, certain bean override
|
||||
* annotations may be declared with an additional {@code ElementType.TYPE} target
|
||||
* for use at the type level, as is the case for {@code @MockitoBean} which can
|
||||
* be declared on a field, test class, or test interface.
|
||||
*
|
||||
* <p>For concrete examples, see
|
||||
* <p>For concrete examples of such composed annotations, see
|
||||
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
|
||||
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and
|
||||
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -104,11 +104,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
Set<String> generatedBeanNames) {
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
|
||||
Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
|
||||
To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
|
||||
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
|
||||
Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
|
||||
To override the bean created by the FactoryBean, remove the '&' prefix."""
|
||||
.formatted(beanName, forField(handler.getField())));
|
||||
|
||||
switch (handler.getStrategy()) {
|
||||
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
|
||||
@@ -134,7 +133,6 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
// 4) Create bean by-name, with a provided name
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (beanName == null) {
|
||||
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
|
||||
@@ -169,11 +167,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
}
|
||||
else if (requireExistingBean) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to replace bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, handler.getBeanType(),
|
||||
field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
Field field = handler.getField();
|
||||
throw new IllegalStateException(
|
||||
"Unable to replace bean: there is no bean with name '%s' and type %s%s."
|
||||
.formatted(beanName, handler.getBeanType(), requiredByField(field)));
|
||||
}
|
||||
// 4) We are creating a bean by-name with the provided beanName.
|
||||
}
|
||||
@@ -264,13 +261,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
else {
|
||||
String message = "Unable to select a bean to wrap: ";
|
||||
if (candidateCount == 0) {
|
||||
message += "there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
|
||||
}
|
||||
else {
|
||||
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames);
|
||||
message += "found %d beans of type %s%s: %s"
|
||||
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
|
||||
}
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
@@ -281,11 +276,9 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
// We are wrapping an existing bean by-name.
|
||||
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
|
||||
if (!candidates.contains(beanName)) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to wrap bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName()));
|
||||
throw new IllegalStateException(
|
||||
"Unable to wrap bean: there is no bean with name '%s' and type %s%s."
|
||||
.formatted(beanName, beanType, requiredByField(field)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,8 +301,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
else if (candidateCount == 0) {
|
||||
if (requireExistingBean) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
"Unable to override bean: there are no beans of type %s%s."
|
||||
.formatted(beanType, requiredByField(field)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -320,14 +313,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames));
|
||||
"Unable to select a bean to override: found %d beans of type %s%s: %s"
|
||||
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
|
||||
}
|
||||
|
||||
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
|
||||
boolean checkAutowiredCandidate) {
|
||||
|
||||
Field field = handler.getField();
|
||||
ResolvableType resolvableType = handler.getBeanType();
|
||||
Class<?> type = resolvableType.toClass();
|
||||
|
||||
@@ -345,16 +338,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
// Filter out non-matching autowire candidates.
|
||||
if (checkAutowiredCandidate) {
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
|
||||
if (field != null && checkAutowiredCandidate) {
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
|
||||
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
|
||||
}
|
||||
// Filter out scoped proxy targets.
|
||||
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
|
||||
|
||||
// In case of multiple matches, fall back on the field's name as a last resort.
|
||||
if (beanNames.size() > 1) {
|
||||
String fieldName = handler.getField().getName();
|
||||
if (field != null && beanNames.size() > 1) {
|
||||
String fieldName = field.getName();
|
||||
if (beanNames.contains(fieldName)) {
|
||||
return Set.of(fieldName);
|
||||
}
|
||||
@@ -452,4 +445,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
dlbf.destroySingleton(beanName);
|
||||
}
|
||||
|
||||
private static String forField(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
}
|
||||
|
||||
private static String requiredByField(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
return " (as required by field '%s.%s')".formatted(
|
||||
field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.Set;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -52,10 +51,7 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
}
|
||||
|
||||
private void findBeanOverrideHandlers(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
|
||||
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
|
||||
findBeanOverrideHandlers(testClass.getEnclosingClass(), handlers);
|
||||
}
|
||||
BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
|
||||
BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
|
||||
Assert.state(handlers.add(handler), () ->
|
||||
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
|
||||
.formatted(testClass.getName(), handler)));
|
||||
|
||||
@@ -17,15 +17,18 @@
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
@@ -35,6 +38,7 @@ 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.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -56,8 +60,8 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
|
||||
*
|
||||
* <p>Concrete implementations of {@code BeanOverrideHandler} can store additional
|
||||
* metadata to use during override {@linkplain #createOverrideInstance instance
|
||||
* creation} — for example, based on further processing of the annotation
|
||||
* or the annotated field.
|
||||
* creation} — for example, based on further processing of the annotation,
|
||||
* the annotated field, or the annotated class.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
|
||||
* Any attempt to override a non-singleton bean will result in an exception.
|
||||
@@ -69,6 +73,11 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
|
||||
*/
|
||||
public abstract class BeanOverrideHandler {
|
||||
|
||||
private static final Comparator<MergedAnnotation<? extends Annotation>> reversedMetaDistance =
|
||||
Comparator.<MergedAnnotation<? extends Annotation>> comparingInt(MergedAnnotation::getDistance).reversed();
|
||||
|
||||
|
||||
@Nullable
|
||||
private final Field field;
|
||||
|
||||
private final Set<Annotation> qualifierAnnotations;
|
||||
@@ -81,7 +90,7 @@ public abstract class BeanOverrideHandler {
|
||||
private final BeanOverrideStrategy strategy;
|
||||
|
||||
|
||||
protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
|
||||
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
|
||||
BeanOverrideStrategy strategy) {
|
||||
|
||||
this.field = field;
|
||||
@@ -95,57 +104,116 @@ public abstract class BeanOverrideHandler {
|
||||
* Process the given {@code testClass} and build the corresponding
|
||||
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
|
||||
* fields in the test class and its type hierarchy.
|
||||
* <p>This method does not search the enclosing class hierarchy.
|
||||
* <p>This method does not search the enclosing class hierarchy and does not
|
||||
* search for {@code @BeanOverride} declarations on classes or interfaces.
|
||||
* @param testClass the test class to process
|
||||
* @return a list of bean override handlers
|
||||
* @see org.springframework.test.context.TestContextAnnotationUtils#searchEnclosingClass(Class)
|
||||
* @see #findAllHandlers(Class)
|
||||
*/
|
||||
public static List<BeanOverrideHandler> forTestClass(Class<?> testClass) {
|
||||
List<BeanOverrideHandler> handlers = new LinkedList<>();
|
||||
findHandlers(testClass, testClass, handlers);
|
||||
return findHandlers(testClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given {@code testClass} and build the corresponding
|
||||
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
|
||||
* fields in the test class and in its type hierarchy as well as from
|
||||
* {@code @BeanOverride} declarations on classes and interfaces.
|
||||
* <p>This method additionally searches for {@code @BeanOverride} declarations
|
||||
* in the enclosing class hierarchy based on
|
||||
* {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics.
|
||||
* @param testClass the test class to process
|
||||
* @return a list of bean override handlers
|
||||
* @since 6.2.2
|
||||
*/
|
||||
static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
|
||||
return findHandlers(testClass, false);
|
||||
}
|
||||
|
||||
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
findHandlers(testClass, testClass, handlers, localFieldsOnly);
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handlers using tail recursion to ensure that "locally declared"
|
||||
* bean overrides take precedence over inherited bean overrides.
|
||||
* Find handlers using tail recursion to ensure that "locally declared" bean overrides
|
||||
* take precedence over inherited bean overrides.
|
||||
* <p>Note: the search algorithm is effectively the inverse of the algorithm used in
|
||||
* {@link org.springframework.test.context.TestContextAnnotationUtils#findAnnotationDescriptor(Class, Class)},
|
||||
* but with tail recursion the semantics should be the same.
|
||||
* @param clazz the class in/on which to search
|
||||
* @param testClass the original test class
|
||||
* @param handlers the list of handlers found
|
||||
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
|
||||
* @since 6.2.2
|
||||
*/
|
||||
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
if (clazz == null || clazz == Object.class) {
|
||||
return;
|
||||
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
|
||||
boolean localFieldsOnly) {
|
||||
|
||||
// 1) Search enclosing class hierarchy.
|
||||
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
|
||||
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
// 1) Search type hierarchy.
|
||||
findHandlers(clazz.getSuperclass(), testClass, handlers);
|
||||
// 2) Search class hierarchy.
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
if (superclass != null && superclass != Object.class) {
|
||||
findHandlers(superclass, testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
// 2) Process fields in current class.
|
||||
if (!localFieldsOnly) {
|
||||
// 3) Search interfaces.
|
||||
for (Class<?> ifc : clazz.getInterfaces()) {
|
||||
findHandlers(ifc, testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
// 4) Process current class.
|
||||
processClass(clazz, testClass, handlers);
|
||||
}
|
||||
|
||||
// 5) Process fields in current class.
|
||||
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
|
||||
}
|
||||
|
||||
private static void processClass(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
processElement(clazz, testClass, (processor, composedAnnotation) ->
|
||||
processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add));
|
||||
}
|
||||
|
||||
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
|
||||
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
|
||||
processElement(field, testClass, (processor, composedAnnotation) -> {
|
||||
Assert.state(!Modifier.isStatic(field.getModifiers()),
|
||||
() -> "@BeanOverride field must not be static: " + field);
|
||||
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);
|
||||
BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, field);
|
||||
handlers.add(handler);
|
||||
handlers.add(processor.createHandler(composedAnnotation, testClass, field));
|
||||
});
|
||||
}
|
||||
|
||||
private static void processElement(AnnotatedElement element, Class<?> testClass,
|
||||
BiConsumer<BeanOverrideProcessor, Annotation> consumer) {
|
||||
|
||||
MergedAnnotations.from(element, DIRECT)
|
||||
.stream(BeanOverride.class)
|
||||
.sorted(reversedMetaDistance)
|
||||
.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();
|
||||
consumer.accept(processor, composedAnnotation);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the annotated {@link Field}.
|
||||
*/
|
||||
@Nullable
|
||||
public final Field getField() {
|
||||
return this.field;
|
||||
}
|
||||
@@ -249,7 +317,10 @@ public abstract class BeanOverrideHandler {
|
||||
}
|
||||
|
||||
// by-type lookup
|
||||
return (Objects.equals(this.field.getName(), that.field.getName()) &&
|
||||
if (this.field == null) {
|
||||
return (that.field == null);
|
||||
}
|
||||
return (that.field != null && this.field.getName().equals(that.field.getName()) &&
|
||||
this.qualifierAnnotations.equals(that.qualifierAnnotations));
|
||||
}
|
||||
|
||||
@@ -257,7 +328,7 @@ public abstract class BeanOverrideHandler {
|
||||
public int hashCode() {
|
||||
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
|
||||
return (this.beanName != null ? hash : hash +
|
||||
Objects.hash(this.field.getName(), this.qualifierAnnotations));
|
||||
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -271,7 +342,10 @@ public abstract class BeanOverrideHandler {
|
||||
}
|
||||
|
||||
|
||||
private static Set<Annotation> getQualifierAnnotations(Field field) {
|
||||
private static Set<Annotation> getQualifierAnnotations(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Annotation[] candidates = field.getDeclaredAnnotations();
|
||||
if (candidates.length == 0) {
|
||||
return Collections.emptySet();
|
||||
|
||||
@@ -18,10 +18,13 @@ package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Strategy interface for Bean Override processing, which creates a
|
||||
* {@link BeanOverrideHandler} that drives how the target bean is overridden.
|
||||
* Strategy interface for Bean Override processing, which creates
|
||||
* {@link BeanOverrideHandler} instances that drive how target beans are
|
||||
* overridden.
|
||||
*
|
||||
* <p>At least one composed annotation that is meta-annotated with
|
||||
* {@link BeanOverride @BeanOverride} must be a companion of this processor and
|
||||
@@ -40,12 +43,41 @@ public interface BeanOverrideProcessor {
|
||||
|
||||
/**
|
||||
* Create a {@link BeanOverrideHandler} for the given annotated field.
|
||||
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
|
||||
* annotation is declared on a field — for example, if the supplied field
|
||||
* is annotated with {@code @MockitoBean}.
|
||||
* @param overrideAnnotation the composed annotation that declares the
|
||||
* {@link BeanOverride @BeanOverride} annotation which registers this processor
|
||||
* {@code @BeanOverride} annotation which registers this processor
|
||||
* @param testClass the test class to process
|
||||
* @param field the annotated field
|
||||
* @return the {@code BeanOverrideHandler} that should handle the given field
|
||||
* @see #createHandlers(Annotation, Class)
|
||||
*/
|
||||
BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field);
|
||||
|
||||
/**
|
||||
* Create a list of {@link BeanOverrideHandler} instances for the given override
|
||||
* annotation and test class.
|
||||
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
|
||||
* annotation is declared at the type level — for example, if the supplied
|
||||
* test class is annotated with {@code @MockitoBean}.
|
||||
* <p>Note that the test class may not be directly annotated with the override
|
||||
* annotation. For example, the override annotation may have been declared
|
||||
* on an interface, superclass, or enclosing class within the test class
|
||||
* hierarchy.
|
||||
* <p>The default implementation returns an empty list, signaling that this
|
||||
* {@code BeanOverrideProcessor} does not support type-level {@code @BeanOverride}
|
||||
* declarations. Can be overridden by concrete implementations to support
|
||||
* type-level use cases.
|
||||
* @param overrideAnnotation the composed annotation that declares the
|
||||
* {@code @BeanOverride} annotation which registers this processor
|
||||
* @param testClass the test class to process
|
||||
* @return the list of {@code BeanOverrideHandlers} for the annotated class
|
||||
* @since 6.2.2
|
||||
* @see #createHandler(Annotation, Class, Field)
|
||||
*/
|
||||
default List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -108,9 +108,11 @@ class BeanOverrideRegistry {
|
||||
}
|
||||
|
||||
void inject(Object target, BeanOverrideHandler handler) {
|
||||
Field field = handler.getField();
|
||||
Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler);
|
||||
String beanName = this.handlerToBeanNameMap.get(handler);
|
||||
Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler);
|
||||
inject(handler.getField(), target, beanName);
|
||||
inject(field, target, beanName);
|
||||
}
|
||||
|
||||
private void inject(Field field, Object target, String beanName) {
|
||||
|
||||
@@ -38,7 +38,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
|
||||
private final MockReset reset;
|
||||
|
||||
|
||||
protected AbstractMockitoBeanOverrideHandler(Field field, ResolvableType beanType,
|
||||
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
|
||||
@Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) {
|
||||
|
||||
super(field, beanType, beanName, strategy);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
@@ -29,17 +30,37 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @MockitoBean} is an annotation that can be applied to a non-static field
|
||||
* in a test class to override a bean in the test's
|
||||
* {@code @MockitoBean} is an annotation that can be used in test classes to
|
||||
* override beans in a test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* using a Mockito mock.
|
||||
* using Mockito mocks.
|
||||
*
|
||||
* <p>By default, the bean to mock is inferred from the type of the annotated
|
||||
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
|
||||
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
|
||||
* the name of the annotated field will be used as a fallback qualifier.
|
||||
* Alternatively, you can explicitly specify a bean name to mock by setting the
|
||||
* {@link #value() value} or {@link #name() name} attribute.
|
||||
* <p>{@code @MockitoBean} can be applied in the following ways.
|
||||
* <ul>
|
||||
* <li>On a non-static field in a test class or any of its superclasses.</li>
|
||||
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
|
||||
* or in any class in the type hierarchy or enclosing class hierarchy above the
|
||||
* {@code @Nested} test class.</li>
|
||||
* <li>At the type level on a test class or any superclass or implemented interface
|
||||
* in the type hierarchy above the test class.</li>
|
||||
* <li>At the type level on an enclosing class for a {@code @Nested} test class
|
||||
* or on any class or interface in the type hierarchy or enclosing class hierarchy
|
||||
* above the {@code @Nested} test class.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
|
||||
* from the type of the annotated field. If multiple candidates exist, a
|
||||
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
|
||||
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
|
||||
* field will be used as a fallback qualifier. Alternatively, you can explicitly
|
||||
* specify a bean name to mock by setting the {@link #value() value} or
|
||||
* {@link #name() name} attribute.
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
|
||||
* to mock must be supplied via the {@link #types() types} attribute. If multiple
|
||||
* candidates exist, you can explicitly specify a bean name to mock by setting the
|
||||
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
|
||||
* must contain a single type if an explicit bean {@code name} is configured.
|
||||
*
|
||||
* <p>A bean will be created if a corresponding bean does not exist. However, if
|
||||
* you would like for the test to fail when a corresponding bean does not exist,
|
||||
@@ -63,19 +84,29 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* (default visibility), or {@code private} depending on the needs or coding
|
||||
* practices of the project.
|
||||
*
|
||||
* <p>{@code @MockitoBean} fields will be inherited from an enclosing test class by default.
|
||||
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* <p>{@code @MockitoBean} fields and type-level {@code @MockitoBean} declarations
|
||||
* will be inherited from an enclosing test class by default. See
|
||||
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* for details.
|
||||
*
|
||||
* <p>{@code @MockitoBean} may be used as a <em>meta-annotation</em> to create custom
|
||||
* <em>composed annotations</em> — for example, to define common mock
|
||||
* configuration in a single annotation that can be reused across a test suite.
|
||||
* {@code @MockitoBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
|
||||
* annotation at the type level — for example, to mock several beans by
|
||||
* {@link #name() name}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see org.springframework.test.context.bean.override.mockito.MockitoBeans @MockitoBeans
|
||||
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
|
||||
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Target({ElementType.FIELD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Repeatable(MockitoBeans.class)
|
||||
@BeanOverride(MockitoBeanOverrideProcessor.class)
|
||||
public @interface MockitoBean {
|
||||
|
||||
@@ -91,13 +122,27 @@ public @interface MockitoBean {
|
||||
/**
|
||||
* Name of the bean to mock.
|
||||
* <p>If left unspecified, the bean to mock is selected according to the
|
||||
* annotated field's type, taking qualifiers into account if necessary. See
|
||||
* the {@linkplain MockitoBean class-level documentation} for details.
|
||||
* configured {@link #types() types} or the annotated field's type, taking
|
||||
* qualifiers into account if necessary. See the {@linkplain MockitoBean
|
||||
* class-level documentation} for details.
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* One or more types to mock.
|
||||
* <p>Defaults to none.
|
||||
* <p>Each type specified will result in a mock being created and registered
|
||||
* with the {@code ApplicationContext}.
|
||||
* <p>Types must be omitted when the annotation is used on a field.
|
||||
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
|
||||
* can only contain a single value.
|
||||
* @return the types to mock
|
||||
* @since 6.2.2
|
||||
*/
|
||||
Class<?>[] types() default {};
|
||||
|
||||
/**
|
||||
* Extra interfaces that should also be declared by the mock.
|
||||
* <p>Defaults to none.
|
||||
|
||||
@@ -57,13 +57,17 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
|
||||
private final boolean serializable;
|
||||
|
||||
|
||||
MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
this(null, typeToMock, mockitoBean);
|
||||
}
|
||||
|
||||
MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
|
||||
(mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
|
||||
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
|
||||
}
|
||||
|
||||
private MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, @Nullable String beanName,
|
||||
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
|
||||
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, Answers answers,
|
||||
boolean serializable) {
|
||||
|
||||
|
||||
@@ -18,15 +18,20 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link BeanOverrideProcessor} implementation that provides support for
|
||||
* {@link MockitoBean @MockitoBean} and {@link MockitoSpyBean @MockitoSpyBean}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see MockitoBean @MockitoBean
|
||||
* @see MockitoSpyBean @MockitoSpyBean
|
||||
@@ -36,6 +41,8 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
@Override
|
||||
public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
|
||||
Assert.state(mockitoBean.types().length == 0,
|
||||
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
|
||||
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
|
||||
}
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
|
||||
@@ -47,4 +54,23 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
.formatted(field.getDeclaringClass().getName(), field.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
|
||||
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
|
||||
throw new IllegalStateException("""
|
||||
Invalid annotation passed to MockitoBeanOverrideProcessor: \
|
||||
expected @MockitoBean on test class """ + testClass.getName());
|
||||
}
|
||||
Class<?>[] types = mockitoBean.types();
|
||||
Assert.state(types.length > 0,
|
||||
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
|
||||
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
|
||||
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
for (Class<?> type : types) {
|
||||
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Container for {@link MockitoBean @MockitoBean} annotations which allows
|
||||
* {@code @MockitoBean} to be used as a {@linkplain java.lang.annotation.Repeatable
|
||||
* repeatable annotation} at the type level — for example, on test classes
|
||||
* or interfaces implemented by test classes.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.2
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface MockitoBeans {
|
||||
|
||||
MockitoBean[] value();
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user