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:
Sam Brannen
2025-01-03 12:09:04 +02:00
parent 8b6523a35b
commit 9181cce65f
36 changed files with 1380 additions and 145 deletions

View File

@@ -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}.

View File

@@ -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());
}
}

View File

@@ -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)));

View File

@@ -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} &mdash; for example, based on further processing of the annotation
* or the annotated field.
* creation} &mdash; 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();

View File

@@ -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 &mdash; 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 &mdash; 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();
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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> &mdash; 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 &mdash; 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.

View File

@@ -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) {

View File

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

View File

@@ -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 &mdash; 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();
}