Provide first-class support for Bean Overrides with @⁠ContextHierarchy

This commit provides first-class support for Bean Overrides
(@⁠MockitoBean, @⁠MockitoSpyBean, @⁠TestBean, etc.) with
@⁠ContextHierarchy.

Specifically, bean overrides can now specify which ApplicationContext
they target within the context hierarchy by configuring the
`contextName` attribute in the annotation. The `contextName` must match
a corresponding `name` configured via @⁠ContextConfiguration.

For example, the following test class configures the name of the second
hierarchy level to be "child" and simultaneously specifies that the
ExampleService should be wrapped in a Mockito spy in the context named
"child". Consequently, Spring will only attempt to create the spy in
the "child" context and will not attempt to create the spy in the
parent context.

@⁠ExtendWith(SpringExtension.class)
@⁠ContextHierarchy({
    @⁠ContextConfiguration(classes = Config1.class),
    @⁠ContextConfiguration(classes = Config2.class, name = "child")
})
class MockitoSpyBeanContextHierarchyTests {

    @⁠MockitoSpyBean(contextName = "child")
    ExampleService service;

    // ...
}

See gh-33293
See gh-34597
See gh-34726
Closes gh-34723

Signed-off-by: Sam Brannen <104798+sbrannen@users.noreply.github.com>
This commit is contained in:
Sam Brannen
2025-04-10 14:46:50 +02:00
committed by GitHub
parent 3afd551174
commit c168e1c297
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 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)));
}
}
@@ -87,9 +87,33 @@ public abstract class BeanOverrideHandler {
@Nullable
private final 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},
@@ -97,16 +121,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;
}
/**
@@ -247,6 +276,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.
@@ -320,6 +364,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;
}
@@ -339,7 +384,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));
}
@@ -350,6 +395,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();
}
@@ -24,15 +24,22 @@ import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.lang.Nullable;
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)
*/
@@ -120,6 +133,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
@@ -43,9 +43,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;
}
@@ -90,6 +90,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);
}
/**
@@ -39,9 +39,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);
}
@@ -92,6 +93,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");
}