Merge branch '6.2.x'

This commit is contained in:
Sam Brannen
2025-04-10 14:52:22 +02:00
52 changed files with 2972 additions and 77 deletions
@@ -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.
@@ -292,13 +292,18 @@ public @interface ContextConfiguration {
* <p>If not specified the name will be inferred based on the numerical level
* within all declared contexts within the hierarchy.
* <p>This attribute is only applicable when used within a test class hierarchy
* or enclosing class hierarchy that is configured using
* {@code @ContextHierarchy}, in which case the name can be used for
* <em>merging</em> or <em>overriding</em> this configuration with configuration
* of the same name in hierarchy levels defined in superclasses or enclosing
* classes. See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for
* details.
* or enclosing class hierarchy that is configured using {@code @ContextHierarchy},
* in which case the name can be used for <em>merging</em> or <em>overriding</em>
* this configuration with configuration of the same name in hierarchy levels
* defined in superclasses or enclosing classes. As of Spring Framework 6.2.6,
* the name can also be used to identify the configuration in which a
* <em>Bean Override</em> should be applied &mdash; for example,
* {@code @MockitoBean(contextName = "child")}. See the Javadoc for
* {@link ContextHierarchy @ContextHierarchy} for details.
* @since 3.2.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean#contextName @MockitoBean(contextName = ...)
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean#contextName @MockitoSpyBean(contextName = ...)
* @see org.springframework.test.context.bean.override.convention.TestBean#contextName @TestBean(contextName = ...)
*/
String name() default "";
@@ -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.
@@ -29,10 +29,12 @@ import java.lang.annotation.Target;
* ApplicationContexts} for integration tests.
*
* <h3>Examples</h3>
*
* <p>The following JUnit-based examples demonstrate common configuration
* scenarios for integration tests that require the use of context hierarchies.
*
* <h4>Single Test Class with Context Hierarchy</h4>
*
* <p>{@code ControllerIntegrationTests} represents a typical integration testing
* scenario for a Spring MVC web application by declaring a context hierarchy
* consisting of two levels, one for the <em>root</em> {@code WebApplicationContext}
@@ -57,6 +59,7 @@ import java.lang.annotation.Target;
* }</pre>
*
* <h4>Class Hierarchy with Implicit Parent Context</h4>
*
* <p>The following test classes define a context hierarchy within a test class
* hierarchy. {@code AbstractWebTests} declares the configuration for a root
* {@code WebApplicationContext} in a Spring-powered web application. Note,
@@ -83,12 +86,13 @@ import java.lang.annotation.Target;
* public class RestWebServiceTests extends AbstractWebTests {}</pre>
*
* <h4>Class Hierarchy with Merged Context Hierarchy Configuration</h4>
*
* <p>The following classes demonstrate the use of <em>named</em> hierarchy levels
* in order to <em>merge</em> the configuration for specific levels in a context
* hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent}
* and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs
* hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code "parent"}
* and {@code "child"}. {@code ExtendedTests} extends {@code BaseTests} and instructs
* the Spring TestContext Framework to merge the context configuration for the
* {@code child} hierarchy level, simply by ensuring that the names declared via
* {@code "child"} hierarchy level, simply by ensuring that the names declared via
* {@link ContextConfiguration#name} are both {@code "child"}. The result is that
* three application contexts will be loaded: one for {@code "/app-config.xml"},
* one for {@code "/user-config.xml"}, and one for <code>{"/user-config.xml",
@@ -111,6 +115,7 @@ import java.lang.annotation.Target;
* public class ExtendedTests extends BaseTests {}</pre>
*
* <h4>Class Hierarchy with Overridden Context Hierarchy Configuration</h4>
*
* <p>In contrast to the previous example, this example demonstrates how to
* <em>override</em> the configuration for a given named level in a context hierarchy
* by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}.
@@ -131,6 +136,72 @@ import java.lang.annotation.Target;
* )
* public class ExtendedTests extends BaseTests {}</pre>
*
* <h4>Context Hierarchies with Bean Overrides</h4>
*
* <p>When {@code @ContextHierarchy} is used in conjunction with bean overrides such as
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, or
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean},
* it may be desirable or necessary to have the override applied to a single level
* in the context hierarchy. To achieve that, the bean override must specify a
* context name that matches a name configured via {@link ContextConfiguration#name}.
*
* <p>The following test class configures the name of the second hierarchy level to be
* {@code "user-config"} and simultaneously specifies that the {@code UserService} should
* be wrapped in a Mockito spy in the context named {@code "user-config"}. Consequently,
* Spring will only attempt to create the spy in the {@code "user-config"} context and will
* not attempt to create the spy in the parent context.
*
* <pre class="code">
* &#064;ExtendWith(SpringExtension.class)
* &#064;ContextHierarchy({
* &#064;ContextConfiguration(classes = AppConfig.class),
* &#064;ContextConfiguration(classes = UserConfig.class, name = "user-config")
* })
* class IntegrationTests {
*
* &#064;MockitoSpyBean(contextName = "user-config")
* UserService userService;
*
* // ...
* }</pre>
*
* <p>When applying bean overrides in different levels of the context hierarchy, you may
* need to have all of the bean override instances injected into the test class in order
* to interact with them &mdash; for example, to configure stubbing for mocks. However,
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired} will always
* inject a matching bean found in the lowest level of the context hierarchy. Thus, to
* inject bean override instances from specific levels in the context hierarchy, you need
* to annotate fields with appropriate bean override annotations and configure the name
* of the context level.
*
* <p>The following test class configures the names of the hierarchy levels to be
* {@code "parent"} and {@code "child"}. It also declares two {@code PropertyService}
* fields that are configured to create or replace {@code PropertyService} beans with
* Mockito mocks in the respective contexts, named {@code "parent"} and {@code "child"}.
* Consequently, the mock from the {@code "parent"} context will be injected into the
* {@code propertyServiceInParent} field, and the mock from the {@code "child"} context
* will be injected into the {@code propertyServiceInChild} field.
*
* <pre class="code">
* &#064;ExtendWith(SpringExtension.class)
* &#064;ContextHierarchy({
* &#064;ContextConfiguration(classes = ParentConfig.class, name = "parent"),
* &#064;ContextConfiguration(classes = ChildConfig.class, name = "child")
* })
* class IntegrationTests {
*
* &#064;MockitoBean(contextName = "parent")
* PropertyService propertyServiceInParent;
*
* &#064;MockitoBean(contextName = "child")
* PropertyService propertyServiceInChild;
*
* // ...
* }</pre>
*
* <h4>Miscellaneous</h4>
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em>.
*
@@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
public @Nullable BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
// Base the context name on the "closest" @ContextConfiguration declaration
// within the type and enclosing class hierarchies of the test class.
String contextName = configAttributes.get(0).getName();
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
findBeanOverrideHandlers(testClass, handlers);
findBeanOverrideHandlers(testClass, contextName, handlers);
if (handlers.isEmpty()) {
return null;
}
return new BeanOverrideContextCustomizer(handlers);
}
private void findBeanOverrideHandlers(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
Assert.state(handlers.add(handler), () ->
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
private void findBeanOverrideHandlers(Class<?> testClass, @Nullable String contextName, Set<BeanOverrideHandler> handlers) {
BeanOverrideHandler.findAllHandlers(testClass).stream()
// If a handler does not specify a context name, it always gets applied.
// Otherwise, the handler's context name must match the current context name.
.filter(handler -> handler.getContextName().isEmpty() || handler.getContextName().equals(contextName))
.forEach(handler -> Assert.state(handlers.add(handler),
() -> "Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
}
}
@@ -86,9 +86,33 @@ public abstract class BeanOverrideHandler {
private final @Nullable String beanName;
private final String contextName;
private final BeanOverrideStrategy strategy;
/**
* Construct a new {@code BeanOverrideHandler} from the supplied values.
* <p>To provide proper support for
* {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy},
* invoke {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)}
* instead.
* @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride},
* or {@code null} if {@code @BeanOverride} was declared at the type level
* @param beanType the {@linkplain ResolvableType type} of bean to override
* @param beanName the name of the bean to override, or {@code null} to look
* for a single matching bean by type
* @param strategy the {@link BeanOverrideStrategy} to use
* @deprecated As of Spring Framework 6.2.6, in favor of
* {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)}
*/
@Deprecated(since = "6.2.6", forRemoval = true)
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
BeanOverrideStrategy strategy) {
this(field, beanType, beanName, "", strategy);
}
/**
* Construct a new {@code BeanOverrideHandler} from the supplied values.
* @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride},
@@ -96,16 +120,21 @@ public abstract class BeanOverrideHandler {
* @param beanType the {@linkplain ResolvableType type} of bean to override
* @param beanName the name of the bean to override, or {@code null} to look
* for a single matching bean by type
* @param contextName the name of the context hierarchy level in which the
* handler should be applied, or an empty string to indicate that the handler
* should be applied to all application contexts within a context hierarchy
* @param strategy the {@link BeanOverrideStrategy} to use
* @since 6.2.6
*/
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
BeanOverrideStrategy strategy) {
String contextName, BeanOverrideStrategy strategy) {
this.field = field;
this.qualifierAnnotations = getQualifierAnnotations(field);
this.beanType = beanType;
this.beanName = beanName;
this.strategy = strategy;
this.contextName = contextName;
}
/**
@@ -244,6 +273,21 @@ public abstract class BeanOverrideHandler {
return this.beanName;
}
/**
* Get the name of the context hierarchy level in which this handler should
* be applied.
* <p>An empty string indicates that this handler should be applied to all
* application contexts.
* <p>If a context name is configured for this handler, it must match a name
* configured via {@code @ContextConfiguration(name=...)}.
* @since 6.2.6
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
* @see org.springframework.test.context.ContextConfiguration#name()
*/
public final String getContextName() {
return this.contextName;
}
/**
* Get the {@link BeanOverrideStrategy} for this {@code BeanOverrideHandler},
* which influences how and when the bean override instance should be created.
@@ -317,6 +361,7 @@ public abstract class BeanOverrideHandler {
BeanOverrideHandler that = (BeanOverrideHandler) other;
if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) ||
!Objects.equals(this.beanName, that.beanName) ||
!Objects.equals(this.contextName, that.contextName) ||
!Objects.equals(this.strategy, that.strategy)) {
return false;
}
@@ -336,7 +381,7 @@ public abstract class BeanOverrideHandler {
@Override
public int hashCode() {
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.contextName, this.strategy);
return (this.beanName != null ? hash : hash +
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
}
@@ -347,6 +392,7 @@ public abstract class BeanOverrideHandler {
.append("field", this.field)
.append("beanType", this.beanType)
.append("beanName", this.beanName)
.append("contextName", this.contextName)
.append("strategy", this.strategy)
.toString();
}
@@ -25,14 +25,21 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.util.Assert;
import static org.springframework.test.context.bean.override.BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME;
/**
* An internal class used to track {@link BeanOverrideHandler}-related state after
* the bean factory has been processed and to provide lookup facilities to test
* execution listeners.
*
* <p>As of Spring Framework 6.2.6, {@code BeanOverrideRegistry} is hierarchical
* and has access to a potential parent in order to provide first-class support
* for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
@@ -48,10 +55,16 @@ class BeanOverrideRegistry {
private final ConfigurableBeanFactory beanFactory;
@Nullable
private final BeanOverrideRegistry parent;
BeanOverrideRegistry(ConfigurableBeanFactory beanFactory) {
Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null");
this.beanFactory = beanFactory;
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(REGISTRY_BEAN_NAME) ?
parentBeanFactory.getBean(REGISTRY_BEAN_NAME, BeanOverrideRegistry.class) : null);
}
/**
@@ -110,7 +123,7 @@ class BeanOverrideRegistry {
* @param handler the {@code BeanOverrideHandler} that created the bean
* @param requiredType the required bean type
* @return the bean instance, or {@code null} if the provided handler is not
* registered in this registry
* registered in this registry or a parent registry
* @since 6.2.6
* @see #registerBeanOverrideHandler(BeanOverrideHandler, String)
*/
@@ -119,6 +132,9 @@ class BeanOverrideRegistry {
if (beanName != null) {
return this.beanFactory.getBean(beanName, requiredType);
}
if (this.parent != null) {
return this.parent.getBeanForHandler(handler, requiredType);
}
return null;
}
@@ -18,8 +18,10 @@ package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@@ -94,14 +96,25 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(testContext.getTestClass());
if (!handlers.isEmpty()) {
Object testInstance = testContext.getTestInstance();
BeanOverrideRegistry beanOverrideRegistry = testContext.getApplicationContext()
ApplicationContext applicationContext = testContext.getApplicationContext();
Assert.state(applicationContext.containsBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME), () -> """
Test class %s declares @BeanOverride fields %s, but no BeanOverrideHandler has been registered. \
If you are using @ContextHierarchy, ensure that context names for bean overrides match \
configured @ContextConfiguration names.""".formatted(testContext.getTestClass().getSimpleName(),
handlers.stream().map(BeanOverrideHandler::getField).filter(Objects::nonNull)
.map(Field::getName).toList()));
BeanOverrideRegistry beanOverrideRegistry = applicationContext
.getBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME, BeanOverrideRegistry.class);
for (BeanOverrideHandler handler : handlers) {
Field field = handler.getField();
Assert.state(field != null, () -> "BeanOverrideHandler must have a non-null field: " + handler);
Object bean = beanOverrideRegistry.getBeanForHandler(handler, field.getType());
Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler);
Assert.state(bean != null, () -> """
No bean override instance found for BeanOverrideHandler %s. If you are using \
@ContextHierarchy, ensure that context names for bean overrides match configured \
@ContextConfiguration names.""".formatted(handler));
injectField(field, testInstance, bean);
}
}
@@ -99,6 +99,16 @@ import org.springframework.test.context.bean.override.BeanOverride;
* }
* }</code></pre>
*
* <p><strong>WARNING</strong>: Using {@code @TestBean} in conjunction with
* {@code @ContextHierarchy} can lead to undesirable results since each
* {@code @TestBean} will be applied to all context hierarchy levels by default.
* To ensure that a particular {@code @TestBean} is applied to a single context
* hierarchy level, set the {@link #contextName() contextName} to match a
* configured {@code @ContextConfiguration}
* {@link org.springframework.test.context.ContextConfiguration#name() name}.
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <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. When
* overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean
@@ -164,6 +174,19 @@ public @interface TestBean {
*/
String methodName() default "";
/**
* The name of the context hierarchy level in which this {@code @TestBean}
* should be applied.
* <p>Defaults to an empty string which indicates that this {@code @TestBean}
* should be applied to all application contexts.
* <p>If a context name is configured, it must match a name configured via
* {@code @ContextConfiguration(name=...)}.
* @since 6.2.6
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
* @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...)
*/
String contextName() default "";
/**
* Whether to require the existence of the bean being overridden.
* <p>Defaults to {@code false} which means that a bean will be created if a
@@ -44,9 +44,9 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler {
TestBeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
BeanOverrideStrategy strategy, Method factoryMethod) {
String contextName, BeanOverrideStrategy strategy, Method factoryMethod) {
super(field, beanType, beanName, strategy);
super(field, beanType, beanName, contextName, strategy);
this.factoryMethod = factoryMethod;
}
@@ -91,6 +91,7 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("contextName", getContextName())
.append("strategy", getStrategy())
.append("factoryMethod", this.factoryMethod)
.toString();
@@ -82,7 +82,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
}
return new TestBeanOverrideHandler(
field, ResolvableType.forField(field, testClass), beanName, strategy, factoryMethod);
field, ResolvableType.forField(field, testClass), beanName, testBean.contextName(), strategy, factoryMethod);
}
/**
@@ -40,9 +40,10 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
@Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset) {
@Nullable String beanName, String contextName, BeanOverrideStrategy strategy,
MockReset reset) {
super(field, beanType, beanName, strategy);
super(field, beanType, beanName, contextName, strategy);
this.reset = (reset != null ? reset : MockReset.AFTER);
}
@@ -93,6 +94,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("contextName", getContextName())
.append("strategy", getStrategy())
.append("reset", getReset())
.toString();
@@ -74,6 +74,16 @@ import org.springframework.test.context.bean.override.BeanOverride;
* registered directly}) will not be found, and a mocked bean will be added to
* the context alongside the existing dependency.
*
* <p><strong>WARNING</strong>: Using {@code @MockitoBean} in conjunction with
* {@code @ContextHierarchy} can lead to undesirable results since each
* {@code @MockitoBean} will be applied to all context hierarchy levels by default.
* To ensure that a particular {@code @MockitoBean} is applied to a single context
* hierarchy level, set the {@link #contextName() contextName} to match a
* configured {@code @ContextConfiguration}
* {@link org.springframework.test.context.ContextConfiguration#name() name}.
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be mocked.
* Any attempt to mock a non-singleton bean will result in an exception. When
* mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean
@@ -144,6 +154,19 @@ public @interface MockitoBean {
*/
Class<?>[] types() default {};
/**
* The name of the context hierarchy level in which this {@code @MockitoBean}
* should be applied.
* <p>Defaults to an empty string which indicates that this {@code @MockitoBean}
* should be applied to all application contexts.
* <p>If a context name is configured, it must match a name configured via
* {@code @ContextConfiguration(name=...)}.
* @since 6.2.6
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
* @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...)
*/
String contextName() default "";
/**
* Extra interfaces that should also be declared by the mock.
* <p>Defaults to none.
@@ -63,15 +63,15 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
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());
mockitoBean.contextName(), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
}
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, Answers answers,
boolean serializable) {
String contextName, BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces,
Answers answers, boolean serializable) {
super(field, typeToMock, beanName, strategy, reset);
super(field, typeToMock, beanName, contextName, strategy, reset);
Assert.notNull(typeToMock, "'typeToMock' must not be null");
this.extraInterfaces = asClassSet(extraInterfaces);
this.answers = answers;
@@ -160,6 +160,7 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("contextName", getContextName())
.append("strategy", getStrategy())
.append("reset", getReset())
.append("extraInterfaces", getExtraInterfaces())
@@ -67,6 +67,16 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly} as resolvable dependencies.
*
* <p><strong>WARNING</strong>: Using {@code @MockitoSpyBean} in conjunction with
* {@code @ContextHierarchy} can lead to undesirable results since each
* {@code @MockitoSpyBean} will be applied to all context hierarchy levels by default.
* To ensure that a particular {@code @MockitoSpyBean} is applied to a single context
* hierarchy level, set the {@link #contextName() contextName} to match a
* configured {@code @ContextConfiguration}
* {@link org.springframework.test.context.ContextConfiguration#name() name}.
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied. Any attempt
* to create a spy for a non-singleton bean will result in an exception. When
* creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
@@ -136,6 +146,19 @@ public @interface MockitoSpyBean {
*/
Class<?>[] types() default {};
/**
* The name of the context hierarchy level in which this {@code @MockitoSpyBean}
* should be applied.
* <p>Defaults to an empty string which indicates that this {@code @MockitoSpyBean}
* should be applied to all application contexts.
* <p>If a context name is configured, it must match a name configured via
* {@code @ContextConfiguration(name=...)}.
* @since 6.2.6
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
* @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...)
*/
String contextName() default "";
/**
* The reset mode to apply to the spied bean.
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically
@@ -54,7 +54,7 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
BeanOverrideStrategy.WRAP, spyBean.reset());
spyBean.contextName(), BeanOverrideStrategy.WRAP, spyBean.reset());
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}