diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
index 153a3b03435f..f92d5c584afa 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
@@ -47,6 +47,21 @@ the same bean in several test classes, make sure to name the fields consistently
creating unnecessary contexts.
====
+[WARNING]
+====
+Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can
+lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be
+applied to all context hierarchy levels by default. To ensure that a particular
+`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set
+the `contextName` attribute to match a configured `@ContextConfiguration` name – for
+example, `@MockitoBean(contextName = "app-config")` or
+`@MockitoSpyBean(contextName = "app-config")`.
+
+See
+xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
+hierarchies with bean overrides] for further details and examples.
+====
+
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc
index a9cc9ced52dc..4ec33c0c154f 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc
@@ -31,6 +31,19 @@ same bean in several tests, make sure to name the field consistently to avoid cr
unnecessary contexts.
====
+[WARNING]
+====
+Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results
+since each `@TestBean` will be applied to all context hierarchy levels by default. To
+ensure that a particular `@TestBean` is applied to a single context hierarchy level, set
+the `contextName` attribute to match a configured `@ContextConfiguration` name – for
+example, `@TestBean(contextName = "app-config")`.
+
+See
+xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
+hierarchies with bean overrides] for further details and examples.
+====
+
[NOTE]
====
There are no restrictions on the visibility of `@TestBean` fields or factory methods.
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc
index c8d57c4276cb..22f97cc1a0a7 100644
--- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc
@@ -22,8 +22,19 @@ given level in the hierarchy, the configuration resource type (that is, XML conf
files or component classes) must be consistent. Otherwise, it is perfectly acceptable to
have different levels in a context hierarchy configured using different resource types.
-The remaining JUnit Jupiter based examples in this section show common configuration
-scenarios for integration tests that require the use of context hierarchies.
+[NOTE]
+====
+If you use `@DirtiesContext` in a test whose context is configured as part of a context
+hierarchy, you can use the `hierarchyMode` flag to control how the context cache is
+cleared.
+
+For further details, see the discussion of `@DirtiesContext` in
+xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]
+and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
+====
+
+The JUnit Jupiter based examples in this section show common configuration scenarios for
+integration tests that require the use of context hierarchies.
**Single test class with context hierarchy**
--
@@ -229,12 +240,118 @@ Kotlin::
class ExtendedTests : BaseTests() {}
----
======
+--
-.Dirtying a context within a context hierarchy
-NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a
-context hierarchy, you can use the `hierarchyMode` flag to control how the context cache
-is cleared. For further details, see the discussion of `@DirtiesContext` in
-xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the
-{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
+[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]]
+**Context hierarchies with bean overrides**
--
+When `@ContextHierarchy` is used in conjunction with
+xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as
+`@TestBean`, `@MockitoBean`, or `@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 the
+`name` attribute in `@ContextConfiguration`.
+
+The following test class configures the name of the second hierarchy level to be
+`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in
+a Mockito spy in the context named `"user-config"`. Consequently, Spring will only
+attempt to create the spy in the `"user-config"` context and will not attempt to create
+the spy in the parent context.
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @ExtendWith(SpringExtension.class)
+ @ContextHierarchy({
+ @ContextConfiguration(classes = AppConfig.class),
+ @ContextConfiguration(classes = UserConfig.class, name = "user-config")
+ })
+ class IntegrationTests {
+
+ @MockitoSpyBean(contextName = "user-config")
+ UserService userService;
+
+ // ...
+ }
+----
+
+Kotlin::
++
+[source,kotlin,indent=0,subs="verbatim,quotes"]
+----
+ @ExtendWith(SpringExtension::class)
+ @ContextHierarchy(
+ ContextConfiguration(classes = [AppConfig::class]),
+ ContextConfiguration(classes = [UserConfig::class], name = "user-config"))
+ class IntegrationTests {
+
+ @MockitoSpyBean(contextName = "user-config")
+ lateinit var userService: UserService
+
+ // ...
+ }
+----
+======
+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 — for example, to configure stubbing for mocks. However, `@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.
+
+The following test class configures the names of the hierarchy levels to be `"parent"`
+and `"child"`. It also declares two `PropertyService` fields that are configured to
+create or replace `PropertyService` beans with Mockito mocks in the respective contexts,
+named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will
+be injected into the `propertyServiceInParent` field, and the mock from the `"child"`
+context will be injected into the `propertyServiceInChild` field.
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @ExtendWith(SpringExtension.class)
+ @ContextHierarchy({
+ @ContextConfiguration(classes = ParentConfig.class, name = "parent"),
+ @ContextConfiguration(classes = ChildConfig.class, name = "child")
+ })
+ class IntegrationTests {
+
+ @MockitoBean(contextName = "parent")
+ PropertyService propertyServiceInParent;
+
+ @MockitoBean(contextName = "child")
+ PropertyService propertyServiceInChild;
+
+ // ...
+ }
+----
+
+Kotlin::
++
+[source,kotlin,indent=0,subs="verbatim,quotes"]
+----
+ @ExtendWith(SpringExtension::class)
+ @ContextHierarchy(
+ ContextConfiguration(classes = [ParentConfig::class], name = "parent"),
+ ContextConfiguration(classes = [ChildConfig::class], name = "child"))
+ class IntegrationTests {
+
+ @MockitoBean(contextName = "parent")
+ lateinit var propertyServiceInParent: PropertyService
+
+ @MockitoBean(contextName = "child")
+ lateinit var propertyServiceInChild: PropertyService
+
+ // ...
+ }
+----
+======
+--
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
index 90bb738b7774..9be2ea22a4b8 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
@@ -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 @@
*
If not specified the name will be inferred based on the numerical level
* within all declared contexts within the hierarchy.
*
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
- * merging or overriding 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 merging or overriding
+ * 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
+ * Bean Override should be applied — 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 "";
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
index 0785c965f8c8..9df6e324e68e 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
@@ -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 @@
* ApplicationContexts} for integration tests.
*
*
Examples
+ *
* The following JUnit-based examples demonstrate common configuration
* scenarios for integration tests that require the use of context hierarchies.
*
*
Single Test Class with Context Hierarchy
+ *
* {@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 root {@code WebApplicationContext}
@@ -57,6 +59,7 @@
* }
*
*
Class Hierarchy with Implicit Parent Context
+ *
* 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 @@
* public class RestWebServiceTests extends AbstractWebTests {}
*
*
Class Hierarchy with Merged Context Hierarchy Configuration
+ *
* The following classes demonstrate the use of named hierarchy levels
* in order to merge 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 {"/user-config.xml",
@@ -111,6 +115,7 @@
* public class ExtendedTests extends BaseTests {}
*
* Class Hierarchy with Overridden Context Hierarchy Configuration
+ *
* In contrast to the previous example, this example demonstrates how to
* override 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 @@
* )
* public class ExtendedTests extends BaseTests {}
*
+ *
Context Hierarchies with Bean Overrides
+ *
+ * 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}.
+ *
+ *
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.
+ *
+ *
+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ * @ContextConfiguration(classes = AppConfig.class),
+ * @ContextConfiguration(classes = UserConfig.class, name = "user-config")
+ * })
+ * class IntegrationTests {
+ *
+ * @MockitoSpyBean(contextName = "user-config")
+ * UserService userService;
+ *
+ * // ...
+ * }
+ *
+ * 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 — 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.
+ *
+ *
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.
+ *
+ *
+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ * @ContextConfiguration(classes = ParentConfig.class, name = "parent"),
+ * @ContextConfiguration(classes = ChildConfig.class, name = "child")
+ * })
+ * class IntegrationTests {
+ *
+ * @MockitoBean(contextName = "parent")
+ * PropertyService propertyServiceInParent;
+ *
+ * @MockitoBean(contextName = "child")
+ * PropertyService propertyServiceInChild;
+ *
+ * // ...
+ * }
+ *
+ * Miscellaneous
+ *
* This annotation may be used as a meta-annotation to create custom
* composed annotations.
*
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
index dfa9c9589eef..ccefe01a8dda 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
@@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
public BeanOverrideContextCustomizer createContextCustomizer(Class> testClass,
List 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 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 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 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)));
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
index 82d76e388324..4fb5b3c2704f 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
@@ -87,26 +87,55 @@ 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.
+ * 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},
+ * 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 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,
+ 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 final String getBeanName() {
return this.beanName;
}
+ /**
+ * Get the name of the context hierarchy level in which this handler should
+ * be applied.
+ *
An empty string indicates that this handler should be applied to all
+ * application contexts.
+ *
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 boolean equals(Object other) {
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 boolean equals(Object other) {
@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 String toString() {
.append("field", this.field)
.append("beanType", this.beanType)
.append("beanName", this.beanName)
+ .append("contextName", this.contextName)
.append("strategy", this.strategy)
.toString();
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
index d9c6deb64471..dd94e9e346f6 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
@@ -24,15 +24,22 @@
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.
*
+ *
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 @@ Object wrapBeanIfNecessary(Object bean, String beanName) {
* @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 @@ Object getBeanForHandler(BeanOverrideHandler handler, Class> requiredType) {
if (beanName != null) {
return this.beanFactory.getBean(beanName, requiredType);
}
+ if (this.parent != null) {
+ return this.parent.getBeanForHandler(handler, requiredType);
+ }
return null;
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
index ca0499c875d3..d3d74ffab02d 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
@@ -18,8 +18,10 @@
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 @@ private static void injectFields(TestContext testContext) {
List 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);
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
index 9393a17ed0cb..837b975b331f 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
@@ -99,6 +99,16 @@
* }
* }
*
+ *
WARNING: 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.
+ *
*
NOTE: Only singleton 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 @@
*/
String methodName() default "";
+ /**
+ * The name of the context hierarchy level in which this {@code @TestBean}
+ * should be applied.
+ *
Defaults to an empty string which indicates that this {@code @TestBean}
+ * should be applied to all application contexts.
+ *
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.
*
Defaults to {@code false} which means that a bean will be created if a
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java
index 20df24ea8850..b372fdcf52a4 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java
@@ -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 @@ public String toString() {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
+ .append("contextName", getContextName())
.append("strategy", getStrategy())
.append("factoryMethod", this.factoryMethod)
.toString();
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
index a47d491b8452..601afcec098f 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
@@ -82,7 +82,7 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas
}
return new TestBeanOverrideHandler(
- field, ResolvableType.forField(field, testClass), beanName, strategy, factoryMethod);
+ field, ResolvableType.forField(field, testClass), beanName, testBean.contextName(), strategy, factoryMethod);
}
/**
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
index 061a3bff4343..4a89a321268b 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
@@ -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 @@ public String toString() {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
+ .append("contextName", getContextName())
.append("strategy", getStrategy())
.append("reset", getReset())
.toString();
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
index 46d5c0917f9c..4c95a21518fa 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
@@ -74,6 +74,16 @@
* registered directly}) will not be found, and a mocked bean will be added to
* the context alongside the existing dependency.
*
+ *
WARNING: 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.
+ *
*
NOTE: Only singleton 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 @@
*/
Class>[] types() default {};
+ /**
+ * The name of the context hierarchy level in which this {@code @MockitoBean}
+ * should be applied.
+ *
Defaults to an empty string which indicates that this {@code @MockitoBean}
+ * should be applied to all application contexts.
+ *
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.
*
Defaults to none.
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
index 449e487e88ba..e76c193fa53c 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
@@ -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 @@ public String toString() {
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
+ .append("contextName", getContextName())
.append("strategy", getStrategy())
.append("reset", getReset())
.append("extraInterfaces", getExtraInterfaces())
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
index e42c0b4563ba..aa2d8cbb59e0 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
@@ -67,6 +67,16 @@
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly} as resolvable dependencies.
*
+ *
WARNING: 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.
+ *
*
NOTE: Only singleton 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 @@
*/
Class>[] types() default {};
+ /**
+ * The name of the context hierarchy level in which this {@code @MockitoSpyBean}
+ * should be applied.
+ *
Defaults to an empty string which indicates that this {@code @MockitoSpyBean}
+ * should be applied to all application contexts.
+ *
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.
*
The default is {@link MockReset#AFTER} meaning that spies are automatically
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
index ce3f11cbe204..5ec896fe0cd3 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
@@ -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");
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java
index 2ed2498993e2..7bc3ce87a36d 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java
@@ -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.
@@ -16,12 +16,13 @@
package org.springframework.test.context.bean.override;
-import java.util.Collections;
+import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
+import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler;
import static org.assertj.core.api.Assertions.assertThat;
@@ -92,7 +93,7 @@ private Consumer dummyHandler(@Nullable String beanName, Cl
@Nullable
private BeanOverrideContextCustomizer createContextCustomizer(Class> testClass) {
- return this.factory.createContextCustomizer(testClass, Collections.emptyList());
+ return this.factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass)));
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java
index 9e01f72ca87e..e99ac7363ae2 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java
@@ -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.
@@ -16,10 +16,11 @@
package org.springframework.test.context.bean.override;
-import java.util.Collections;
+import java.util.List;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
+import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
@@ -44,7 +45,7 @@ public abstract class BeanOverrideContextCustomizerTestUtils {
*/
@Nullable
public static ContextCustomizer createContextCustomizer(Class> testClass) {
- return factory.createContextCustomizer(testClass, Collections.emptyList());
+ return factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass)));
}
/**
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java
index 8944aeb2be3f..57fc29c8ff15 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java
@@ -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.
@@ -72,7 +72,7 @@ private static class DummyBeanOverrideHandler extends BeanOverrideHandler {
public DummyBeanOverrideHandler(String key) {
super(ReflectionUtils.findField(DummyBeanOverrideHandler.class, "key"),
- ResolvableType.forClass(Object.class), null, BeanOverrideStrategy.REPLACE);
+ ResolvableType.forClass(Object.class), null, "", BeanOverrideStrategy.REPLACE);
this.key = key;
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java
index 5229cf5b44dd..2ed874f460e1 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java
@@ -30,6 +30,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.Nullable;
+import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor;
import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler;
import org.springframework.test.context.bean.override.example.CustomQualifier;
import org.springframework.test.context.bean.override.example.ExampleService;
@@ -116,7 +117,7 @@ void isEqualToWithSameMetadata() {
}
@Test
- void isEqualToWithSameMetadataAndBeanNames() {
+ void isEqualToWithSameMetadataAndSameBeanNames() {
BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean");
BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean");
assertThat(handler1).isEqualTo(handler2);
@@ -124,10 +125,29 @@ void isEqualToWithSameMetadataAndBeanNames() {
}
@Test
- void isNotEqualToWithSameMetadataAndDifferentBeaName() {
+ void isNotEqualToWithSameMetadataButDifferentBeanNames() {
BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean");
BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean2");
assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
+ }
+
+ @Test
+ void isEqualToWithSameMetadataSameBeanNamesAndSameContextNames() {
+ Class> testClass = MultipleAnnotationsWithSameNameInDifferentContext.class;
+ BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean"));
+ BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean2"));
+ assertThat(handler1).isEqualTo(handler2);
+ assertThat(handler1).hasSameHashCodeAs(handler2);
+ }
+
+ @Test
+ void isEqualToWithSameMetadataAndSameBeanNamesButDifferentContextNames() {
+ Class> testClass = MultipleAnnotationsWithSameNameInDifferentContext.class;
+ BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean"));
+ BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "childMessageBean"));
+ assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test
@@ -173,6 +193,7 @@ void isNotEqualToWithSameMetadataAndDifferentQualifierValues() {
BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier"));
BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "differentDirectQualifier"));
assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test
@@ -180,6 +201,7 @@ void isNotEqualToWithSameMetadataAndDifferentQualifiers() {
BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier"));
BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "customQualifier"));
assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test
@@ -187,6 +209,7 @@ void isNotEqualToWithByTypeLookupAndDifferentFieldNames() {
BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"));
BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigB.class, "example"));
assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
private static BeanOverrideHandler createBeanOverrideHandler(Field field) {
@@ -194,7 +217,11 @@ private static BeanOverrideHandler createBeanOverrideHandler(Field field) {
}
private static BeanOverrideHandler createBeanOverrideHandler(Field field, @Nullable String name) {
- return new DummyBeanOverrideHandler(field, field.getType(), name, BeanOverrideStrategy.REPLACE);
+ return new DummyBeanOverrideHandler(field, field.getType(), name, "", BeanOverrideStrategy.REPLACE);
+ }
+
+ private static BeanOverrideHandler createBeanOverrideHandler(Class> testClass, Field field) {
+ return new DummyBeanOverrideProcessor().createHandler(field.getAnnotation(DummyBean.class), testClass, field);
}
private static Field field(Class> target, String fieldName) {
@@ -234,6 +261,18 @@ static class MultipleAnnotations {
Integer counter;
}
+ static class MultipleAnnotationsWithSameNameInDifferentContext {
+
+ @DummyBean(beanName = "messageBean", contextName = "parent")
+ String parentMessageBean;
+
+ @DummyBean(beanName = "messageBean", contextName = "parent")
+ String parentMessageBean2;
+
+ @DummyBean(beanName = "messageBean", contextName = "child")
+ String childMessageBean;
+ }
+
static class MultipleAnnotationsDuplicate {
@DummyBean(beanName = "messageBean")
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java
new file mode 100644
index 000000000000..cb0018d3a208
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.platform.testkit.engine.EngineTestKit;
+import org.junit.platform.testkit.engine.Events;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.testkit.engine.EventConditions.event;
+import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
+import static org.junit.platform.testkit.engine.EventConditions.test;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
+
+/**
+ * Integration tests for {@link BeanOverrideTestExecutionListener}.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+class BeanOverrideTestExecutionListenerTests {
+
+ @Test
+ void beanOverrideWithNoMatchingContextName() {
+ executeTests(BeanOverrideWithNoMatchingContextNameTestCase.class)
+ .assertThatEvents().haveExactly(1, event(test("test"),
+ finishedWithFailure(
+ instanceOf(IllegalStateException.class),
+ message("""
+ Test class BeanOverrideWithNoMatchingContextNameTestCase declares @BeanOverride \
+ fields [message, number], but no BeanOverrideHandler has been registered. \
+ If you are using @ContextHierarchy, ensure that context names for bean overrides match \
+ configured @ContextConfiguration names."""))));
+ }
+
+ @Test
+ void beanOverrideWithInvalidContextName() {
+ executeTests(BeanOverrideWithInvalidContextNameTestCase.class)
+ .assertThatEvents().haveExactly(1, event(test("test"),
+ finishedWithFailure(
+ instanceOf(IllegalStateException.class),
+ message(msg ->
+ msg.startsWith("No bean override instance found for BeanOverrideHandler") &&
+ msg.contains("DummyBeanOverrideHandler") &&
+ msg.contains("BeanOverrideWithInvalidContextNameTestCase.message2") &&
+ msg.contains("contextName = 'BOGUS'") &&
+ msg.endsWith("""
+ If you are using @ContextHierarchy, ensure that context names for bean overrides match \
+ configured @ContextConfiguration names.""")))));
+ }
+
+
+ private static Events executeTests(Class> testClass) {
+ return EngineTestKit.engine("junit-jupiter")
+ .selectors(selectClass(testClass))
+ .execute()
+ .testEvents()
+ .assertStatistics(stats -> stats.started(1).failed(1));
+ }
+
+
+ @ExtendWith(SpringExtension.class)
+ @ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+ })
+ @DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+ static class BeanOverrideWithNoMatchingContextNameTestCase {
+
+ @DummyBean(contextName = "BOGUS")
+ String message;
+
+ @DummyBean(contextName = "BOGUS")
+ Integer number;
+
+ @Test
+ void test() {
+ // no-op
+ }
+ }
+
+ @ExtendWith(SpringExtension.class)
+ @ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+ })
+ @DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+ static class BeanOverrideWithInvalidContextNameTestCase {
+
+ @DummyBean(contextName = "child")
+ String message1;
+
+ @DummyBean(contextName = "BOGUS")
+ String message2;
+
+ @Test
+ void test() {
+ // no-op
+ }
+ }
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ String message() {
+ return "Message 1";
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ String message() {
+ return "Message 2";
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java
index d6beaf4ba306..ef2e1f45cc6b 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java
@@ -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.
@@ -32,7 +32,7 @@
/**
* A dummy {@link BeanOverride} implementation that only handles {@link CharSequence}
- * and {@link Integer} and replace them with {@code "overridden"} and {@code 42},
+ * and {@link Integer} and replaces them with {@code "overridden"} and {@code 42},
* respectively.
*
* @author Stephane Nicoll
@@ -45,6 +45,8 @@
String beanName() default "";
+ String contextName() default "";
+
BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE;
class DummyBeanOverrideProcessor implements BeanOverrideProcessor {
@@ -54,7 +56,7 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class> testCla
DummyBean dummyBean = (DummyBean) annotation;
String beanName = (StringUtils.hasText(dummyBean.beanName()) ? dummyBean.beanName() : null);
return new DummyBeanOverrideProcessor.DummyBeanOverrideHandler(field, field.getType(), beanName,
- dummyBean.strategy());
+ dummyBean.contextName(), dummyBean.strategy());
}
// Bare bone, "dummy", implementation that should not override anything
@@ -62,9 +64,9 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class> testCla
static class DummyBeanOverrideHandler extends BeanOverrideHandler {
DummyBeanOverrideHandler(Field field, Class> typeToOverride, @Nullable String beanName,
- BeanOverrideStrategy strategy) {
+ String contextName, BeanOverrideStrategy strategy) {
- super(field, ResolvableType.forClass(typeToOverride), beanName, strategy);
+ super(field, ResolvableType.forClass(typeToOverride), beanName, contextName, strategy);
}
@Override
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java
index f95fe62912a7..b47ea30c1305 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java
@@ -130,7 +130,7 @@ private static TestBeanOverrideHandler handlerFor(Field field, Method overrideMe
TestBean annotation = field.getAnnotation(TestBean.class);
String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null);
return new TestBeanOverrideHandler(
- field, ResolvableType.forClass(field.getType()), beanName, BeanOverrideStrategy.REPLACE, overrideMethod);
+ field, ResolvableType.forClass(field.getType()), beanName, "", BeanOverrideStrategy.REPLACE, overrideMethod);
}
static class SampleOneOverride {
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..a59c59bfa0e0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only overridden "by name" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByNameInChildContextHierarchyTests {
+
+ @TestBean(name = "service", contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService service() {
+ return () -> "@TestBean 2";
+ }
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertThat(service.greeting()).isEqualTo("@TestBean 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 2";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..8df069273a40
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,114 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are overridden "by name" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByNameInParentAndChildContextHierarchyTests {
+
+ @TestBean(name = "service", contextName = "parent")
+ ExampleService serviceInParent;
+
+ @TestBean(name = "service", contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService serviceInParent() {
+ return () -> "@TestBean 1";
+ }
+
+ static ExampleService serviceInChild() {
+ return () -> "@TestBean 2";
+ }
+
+
+ @Test
+ void test() {
+ assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 2";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..e2f3ec516c60
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java
@@ -0,0 +1,101 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only overridden "by name" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByNameInParentContextHierarchyTests {
+
+ @TestBean(name = "service", contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService service() {
+ return () -> "@TestBean 1";
+ }
+
+
+ @Test
+ void test() {
+ assertThat(service.greeting()).isEqualTo("@TestBean 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..b1e8461fe032
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only overridden "by type" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByTypeInChildContextHierarchyTests {
+
+ @TestBean(contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService service() {
+ return () -> "@TestBean 2";
+ }
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertThat(service.greeting()).isEqualTo("@TestBean 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 2";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..b7e021528eb9
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,114 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are overridden "by type" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByTypeInParentAndChildContextHierarchyTests {
+
+ @TestBean(contextName = "parent")
+ ExampleService serviceInParent;
+
+ @TestBean(contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService serviceInParent() {
+ return () -> "@TestBean 1";
+ }
+
+ static ExampleService serviceInChild() {
+ return () -> "@TestBean 2";
+ }
+
+
+ @Test
+ void test() {
+ assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 2";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..5fb01297c768
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java
@@ -0,0 +1,101 @@
+/*
+ * 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.convention.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.convention.TestBean;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only overridden "by type" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class TestBeanByTypeInParentContextHierarchyTests {
+
+ @TestBean(contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ static ExampleService service() {
+ return () -> "@TestBean 1";
+ }
+
+
+ @Test
+ void test() {
+ assertThat(service.greeting()).isEqualTo("@TestBean 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return () -> "Service 1";
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java
index d93fafb7837d..6c0352c5317a 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java
@@ -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.
@@ -43,7 +43,7 @@ class EasyMockBeanOverrideHandler extends BeanOverrideHandler {
EasyMockBeanOverrideHandler(Field field, Class> typeToOverride, @Nullable String beanName,
MockType mockType) {
- super(field, ResolvableType.forClass(typeToOverride), beanName, REPLACE_OR_CREATE);
+ super(field, ResolvableType.forClass(typeToOverride), beanName, "", REPLACE_OR_CREATE);
this.mockType = mockType;
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java
new file mode 100644
index 000000000000..1b71868b4bbe
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java
@@ -0,0 +1,24 @@
+/*
+ * 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.hierarchies;
+
+class BarService {
+
+ String bar() {
+ return "bar";
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java
new file mode 100644
index 000000000000..5877553e1acf
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java
@@ -0,0 +1,37 @@
+/*
+ * 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.hierarchies;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class ErrorIfContextReloadedConfig {
+
+ private static boolean loaded = false;
+
+
+ @PostConstruct
+ public void postConstruct() {
+ if (loaded) {
+ throw new RuntimeException("Context loaded multiple times");
+ }
+ loaded = true;
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java
new file mode 100644
index 000000000000..ab2ee99fc965
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java
@@ -0,0 +1,24 @@
+/*
+ * 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.hierarchies;
+
+class FooService {
+
+ String foo() {
+ return "foo";
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java
similarity index 90%
rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java
rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java
index 00950dcd03fd..98d633a12b70 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java
@@ -14,12 +14,11 @@
* limitations under the License.
*/
-package org.springframework.test.context.bean.override.mockito.integration;
+package org.springframework.test.context.bean.override.mockito.hierarchies;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.bean.override.example.ExampleService;
@@ -45,8 +44,6 @@ public class MockitoBeanAndContextHierarchyParentIntegrationTests {
@MockitoBean
ExampleService service;
- @Autowired
- ApplicationContext context;
@BeforeEach
void configureServiceMock() {
@@ -54,7 +51,7 @@ void configureServiceMock() {
}
@Test
- void test() {
+ void test(ApplicationContext context) {
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty();
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..e452b50830f6
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java
@@ -0,0 +1,111 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only mocked "by name" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByNameInChildContextHierarchyTests {
+
+ @MockitoBean(name = "service", contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertIsNotMock(serviceInParent);
+
+ when(service.greeting()).thenReturn("Mock 2");
+
+ assertThat(service.greeting()).isEqualTo("Mock 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..539611a27f4b
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are mocked "by name" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByNameInParentAndChildContextHierarchyTests {
+
+ @MockitoBean(name = "service", contextName = "parent")
+ ExampleService serviceInParent;
+
+ @MockitoBean(name = "service", contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ when(serviceInParent.greeting()).thenReturn("Mock 1");
+ when(serviceInChild.greeting()).thenReturn("Mock 2");
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Mock 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Mock 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..01832db8fd23
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java
@@ -0,0 +1,100 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only mocked "by name" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByNameInParentContextHierarchyTests {
+
+ @MockitoBean(name = "service", contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ when(service.greeting()).thenReturn("Mock 1");
+
+ assertThat(service.greeting()).isEqualTo("Mock 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..d6421b208146
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java
@@ -0,0 +1,111 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only mocked "by type" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByTypeInChildContextHierarchyTests {
+
+ @MockitoBean(contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertIsNotMock(serviceInParent);
+
+ when(service.greeting()).thenReturn("Mock 2");
+
+ assertThat(service.greeting()).isEqualTo("Mock 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..f212d309a803
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are mocked "by type" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByTypeInParentAndChildContextHierarchyTests {
+
+ @MockitoBean(contextName = "parent")
+ ExampleService serviceInParent;
+
+ @MockitoBean(contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ when(serviceInParent.greeting()).thenReturn("Mock 1");
+ when(serviceInChild.greeting()).thenReturn("Mock 2");
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Mock 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Mock 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..6a1f281cf2da
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java
@@ -0,0 +1,100 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that {@link MockitoBean @MockitoBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only mocked "by type" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoBeanByTypeInParentContextHierarchyTests {
+
+ @MockitoBean(contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ when(service.greeting()).thenReturn("Mock 1");
+
+ assertThat(service.greeting()).isEqualTo("Mock 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java
similarity index 84%
rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java
rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java
index b5f02fa893f0..aef8cd39cbd4 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java
@@ -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.
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package org.springframework.test.context.bean.override.mockito.integration;
+package org.springframework.test.context.bean.override.mockito.hierarchies;
import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -50,18 +49,14 @@ public class MockitoSpyBeanAndContextHierarchyChildIntegrationTests extends
@MockitoSpyBean
ExampleServiceCaller serviceCaller;
- @Autowired
- ApplicationContext context;
-
@Test
@Override
- void test() {
- assertThat(context).as("child ApplicationContext").isNotNull();
- assertThat(context.getParent()).as("parent ApplicationContext").isNotNull();
- assertThat(context.getParent().getParent()).as("grandparent ApplicationContext").isNull();
-
+ void test(ApplicationContext context) {
ApplicationContext parentContext = context.getParent();
+ assertThat(parentContext).as("parent ApplicationContext").isNotNull();
+ assertThat(parentContext.getParent()).as("grandparent ApplicationContext").isNull();
+
assertThat(parentContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
assertThat(parentContext.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty();
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..63c3561d0881
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only spied on "by name" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByNameInChildContextHierarchyTests {
+
+ @MockitoSpyBean(name = "service", contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertIsNotSpy(serviceInParent);
+ assertIsSpy(service);
+
+ assertThat(service.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..255f3630beac
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are spied on "by name" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByNameInParentAndChildContextHierarchyTests {
+
+ @MockitoSpyBean(name = "service", contextName = "parent")
+ ExampleService serviceInParent;
+
+ @MockitoSpyBean(name = "service", contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ assertIsSpy(serviceInParent);
+ assertIsSpy(serviceInChild);
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Service 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java
new file mode 100644
index 000000000000..951d37b96215
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java
@@ -0,0 +1,82 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * This is effectively a one-to-one copy of
+ * {@link MockitoSpyBeanByNameInParentAndChildContextHierarchyTests}, except
+ * that this test class uses different names for the context hierarchy levels:
+ * level-1 and level-2 instead of parent and child.
+ *
+ * If the context cache is broken, either this test class or
+ * {@code MockitoSpyBeanByNameInParentAndChildContextHierarchyTests} will fail
+ * when run within the same test suite.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ * @see MockitoSpyBeanByNameInParentAndChildContextHierarchyTests
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1.class, name = "level-1"),
+ @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2.class, name = "level-2")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests {
+
+ @MockitoSpyBean(name = "service", contextName = "level-1")
+ ExampleService serviceInParent;
+
+ @MockitoSpyBean(name = "service", contextName = "level-2")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ assertIsSpy(serviceInParent);
+ assertIsSpy(serviceInChild);
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Service 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..13e1eba1b12f
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java
@@ -0,0 +1,100 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only spied on "by name" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByNameInParentContextHierarchyTests {
+
+ @MockitoSpyBean(name = "service", contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ assertIsSpy(service);
+
+ assertThat(service.greeting()).isEqualTo("Service 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java
new file mode 100644
index 000000000000..1f71a5e674f1
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only spied on "by type" in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByTypeInChildContextHierarchyTests {
+
+ @MockitoSpyBean(contextName = "child")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertIsNotSpy(serviceInParent);
+ assertIsSpy(service);
+
+ assertThat(service.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..90cf7d285699
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are spied on "by type" in the parent and in the child.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests {
+
+ @MockitoSpyBean(contextName = "parent")
+ ExampleService serviceInParent;
+
+ @MockitoSpyBean(contextName = "child")
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ assertIsSpy(serviceInParent);
+ assertIsSpy(serviceInChild);
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Service 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java
new file mode 100644
index 000000000000..3d0d841c8f1e
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java
@@ -0,0 +1,100 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * a bean is only spied on "by type" in the parent.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class)
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class MockitoSpyBeanByTypeInParentContextHierarchyTests {
+
+ @MockitoSpyBean(contextName = "parent")
+ ExampleService service;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test() {
+ assertIsSpy(service);
+
+ assertThat(service.greeting()).isEqualTo("Service 1");
+ assertThat(serviceCaller1.getService()).isSameAs(service);
+ assertThat(serviceCaller2.getService()).isSameAs(service);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java
new file mode 100644
index 000000000000..e4fb4d16c3e0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java
@@ -0,0 +1,113 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config1;
+import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config2;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
+
+/**
+ * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a
+ * {@link ContextHierarchy @ContextHierarchy} with named context levels, when
+ * identical beans are spied on "by type" in the parent and in the child and
+ * configured via class-level {@code @MockitoSpyBean} declarations.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = Config1.class, name = "parent"),
+ @ContextConfiguration(classes = Config2.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+@MockitoSpyBean(types = ExampleService.class, contextName = "parent")
+@MockitoSpyBean(types = ExampleService.class, contextName = "child")
+class MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests {
+
+ @Autowired
+ ExampleService serviceInChild;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller1;
+
+ @Autowired
+ ExampleServiceCaller serviceCaller2;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ ExampleService serviceInParent = context.getParent().getBean(ExampleService.class);
+
+ assertIsSpy(serviceInParent);
+ assertIsSpy(serviceInChild);
+
+ assertThat(serviceInParent.greeting()).isEqualTo("Service 1");
+ assertThat(serviceInChild.greeting()).isEqualTo("Service 2");
+ assertThat(serviceCaller1.getService()).isSameAs(serviceInParent);
+ assertThat(serviceCaller2.getService()).isSameAs(serviceInChild);
+ assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1");
+ assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2");
+ }
+
+
+ @Configuration
+ static class Config1 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 1");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller1(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+ @Configuration
+ static class Config2 {
+
+ @Bean
+ ExampleService service() {
+ return new RealExampleService("Service 2");
+ }
+
+ @Bean
+ ExampleServiceCaller serviceCaller2(ExampleService service) {
+ return new ExampleServiceCaller(service);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java
new file mode 100644
index 000000000000..b5dc403a727b
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+/**
+ * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is
+ * loaded twice (i.e., not properly cached), either this test class or
+ * {@link ReusedParentConfigV2Tests} will fail when both test classes are run
+ * within the same test suite.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class),
+ @ContextConfiguration(classes = FooService.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class ReusedParentConfigV1Tests {
+
+ @Autowired
+ ErrorIfContextReloadedConfig sharedConfig;
+
+ @MockitoBean(contextName = "child")
+ FooService fooService;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ assertThat(context.getParent().getBeanNamesForType(FooService.class)).isEmpty();
+ assertThat(context.getBeanNamesForType(FooService.class)).hasSize(1);
+
+ given(fooService.foo()).willReturn("mock");
+ assertThat(fooService.foo()).isEqualTo("mock");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java
new file mode 100644
index 000000000000..009d85e16353
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hierarchies;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+import org.springframework.test.context.aot.DisabledInAotMode;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+/**
+ * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is
+ * loaded twice (i.e., not properly cached), either this test class or
+ * {@link ReusedParentConfigV1Tests} will fail when both test classes are run
+ * within the same test suite.
+ *
+ * @author Sam Brannen
+ * @since 6.2.6
+ */
+@ExtendWith(SpringExtension.class)
+@ContextHierarchy({
+ @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class),
+ @ContextConfiguration(classes = BarService.class, name = "child")
+})
+@DisabledInAotMode("@ContextHierarchy is not supported in AOT")
+class ReusedParentConfigV2Tests {
+
+ @Autowired
+ ErrorIfContextReloadedConfig sharedConfig;
+
+ @MockitoBean(contextName = "child")
+ BarService barService;
+
+
+ @Test
+ void test(ApplicationContext context) {
+ assertThat(context.getParent().getBeanNamesForType(BarService.class)).isEmpty();
+ assertThat(context.getBeanNamesForType(BarService.class)).hasSize(1);
+
+ given(barService.bar()).willReturn("mock");
+ assertThat(barService.bar()).isEqualTo("mock");
+ }
+
+}