Skip to content

Provide first-class support for Bean Overrides with @ContextHierarchy #34723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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**
--
Expand Down Expand Up @@ -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

// ...
}
----
======
--
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -292,13 +292,18 @@
* <p>If not specified the name will be inferred based on the numerical level
* within all declared contexts within the hierarchy.
* <p>This attribute is only applicable when used within a test class hierarchy
* or enclosing class hierarchy that is configured using
* {@code @ContextHierarchy}, in which case the name can be used for
* <em>merging</em> or <em>overriding</em> this configuration with configuration
* of the same name in hierarchy levels defined in superclasses or enclosing
* classes. See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for
* details.
* or enclosing class hierarchy that is configured using {@code @ContextHierarchy},
* in which case the name can be used for <em>merging</em> or <em>overriding</em>
* this configuration with configuration of the same name in hierarchy levels
* defined in superclasses or enclosing classes. As of Spring Framework 6.2.6,
* the name can also be used to identify the configuration in which a
* <em>Bean Override</em> should be applied &mdash; for example,
* {@code @MockitoBean(contextName = "child")}. See the Javadoc for
* {@link ContextHierarchy @ContextHierarchy} for details.
* @since 3.2.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean#contextName @MockitoBean(contextName = ...)
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean#contextName @MockitoSpyBean(contextName = ...)
* @see org.springframework.test.context.bean.override.convention.TestBean#contextName @TestBean(contextName = ...)
*/
String name() default "";

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -29,10 +29,12 @@
* ApplicationContexts} for integration tests.
*
* <h3>Examples</h3>
*
* <p>The following JUnit-based examples demonstrate common configuration
* scenarios for integration tests that require the use of context hierarchies.
*
* <h4>Single Test Class with Context Hierarchy</h4>
*
* <p>{@code ControllerIntegrationTests} represents a typical integration testing
* scenario for a Spring MVC web application by declaring a context hierarchy
* consisting of two levels, one for the <em>root</em> {@code WebApplicationContext}
Expand All @@ -57,6 +59,7 @@
* }</pre>
*
* <h4>Class Hierarchy with Implicit Parent Context</h4>
*
* <p>The following test classes define a context hierarchy within a test class
* hierarchy. {@code AbstractWebTests} declares the configuration for a root
* {@code WebApplicationContext} in a Spring-powered web application. Note,
Expand All @@ -83,12 +86,13 @@
* public class RestWebServiceTests extends AbstractWebTests {}</pre>
*
* <h4>Class Hierarchy with Merged Context Hierarchy Configuration</h4>
*
* <p>The following classes demonstrate the use of <em>named</em> hierarchy levels
* in order to <em>merge</em> the configuration for specific levels in a context
* hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent}
* and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs
* hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code "parent"}
* and {@code "child"}. {@code ExtendedTests} extends {@code BaseTests} and instructs
* the Spring TestContext Framework to merge the context configuration for the
* {@code child} hierarchy level, simply by ensuring that the names declared via
* {@code "child"} hierarchy level, simply by ensuring that the names declared via
* {@link ContextConfiguration#name} are both {@code "child"}. The result is that
* three application contexts will be loaded: one for {@code "/app-config.xml"},
* one for {@code "/user-config.xml"}, and one for <code>{"/user-config.xml",
Expand All @@ -111,6 +115,7 @@
* public class ExtendedTests extends BaseTests {}</pre>
*
* <h4>Class Hierarchy with Overridden Context Hierarchy Configuration</h4>
*
* <p>In contrast to the previous example, this example demonstrates how to
* <em>override</em> the configuration for a given named level in a context hierarchy
* by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}.
Expand All @@ -131,6 +136,72 @@
* )
* public class ExtendedTests extends BaseTests {}</pre>
*
* <h4>Context Hierarchies with Bean Overrides</h4>
*
* <p>When {@code @ContextHierarchy} is used in conjunction with bean overrides such as
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, or
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean},
* it may be desirable or necessary to have the override applied to a single level
* in the context hierarchy. To achieve that, the bean override must specify a
* context name that matches a name configured via {@link ContextConfiguration#name}.
*
* <p>The following test class configures the name of the second hierarchy level to be
* {@code "user-config"} and simultaneously specifies that the {@code UserService} should
* be wrapped in a Mockito spy in the context named {@code "user-config"}. Consequently,
* Spring will only attempt to create the spy in the {@code "user-config"} context and will
* not attempt to create the spy in the parent context.
*
* <pre class="code">
* &#064;ExtendWith(SpringExtension.class)
* &#064;ContextHierarchy({
* &#064;ContextConfiguration(classes = AppConfig.class),
* &#064;ContextConfiguration(classes = UserConfig.class, name = "user-config")
* })
* class IntegrationTests {
*
* &#064;MockitoSpyBean(contextName = "user-config")
* UserService userService;
*
* // ...
* }</pre>
*
* <p>When applying bean overrides in different levels of the context hierarchy, you may
* need to have all of the bean override instances injected into the test class in order
* to interact with them &mdash; for example, to configure stubbing for mocks. However,
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired} will always
* inject a matching bean found in the lowest level of the context hierarchy. Thus, to
* inject bean override instances from specific levels in the context hierarchy, you need
* to annotate fields with appropriate bean override annotations and configure the name
* of the context level.
*
* <p>The following test class configures the names of the hierarchy levels to be
* {@code "parent"} and {@code "child"}. It also declares two {@code PropertyService}
* fields that are configured to create or replace {@code PropertyService} beans with
* Mockito mocks in the respective contexts, named {@code "parent"} and {@code "child"}.
* Consequently, the mock from the {@code "parent"} context will be injected into the
* {@code propertyServiceInParent} field, and the mock from the {@code "child"} context
* will be injected into the {@code propertyServiceInChild} field.
*
* <pre class="code">
* &#064;ExtendWith(SpringExtension.class)
* &#064;ContextHierarchy({
* &#064;ContextConfiguration(classes = ParentConfig.class, name = "parent"),
* &#064;ContextConfiguration(classes = ChildConfig.class, name = "child")
* })
* class IntegrationTests {
*
* &#064;MockitoBean(contextName = "parent")
* PropertyService propertyServiceInParent;
*
* &#064;MockitoBean(contextName = "child")
* PropertyService propertyServiceInChild;
*
* // ...
* }</pre>
*
* <h4>Miscellaneous</h4>
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em>.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
public BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {

// Base the context name on the "closest" @ContextConfiguration declaration
// within the type and enclosing class hierarchies of the test class.
String contextName = configAttributes.get(0).getName();
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
findBeanOverrideHandlers(testClass, handlers);
findBeanOverrideHandlers(testClass, contextName, handlers);
if (handlers.isEmpty()) {
return null;
}
return new BeanOverrideContextCustomizer(handlers);
}

private void findBeanOverrideHandlers(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
Assert.state(handlers.add(handler), () ->
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
private void findBeanOverrideHandlers(Class<?> testClass, @Nullable String contextName, Set<BeanOverrideHandler> handlers) {
BeanOverrideHandler.findAllHandlers(testClass).stream()
// If a handler does not specify a context name, it always gets applied.
// Otherwise, the handler's context name must match the current context name.
.filter(handler -> handler.getContextName().isEmpty() || handler.getContextName().equals(contextName))
.forEach(handler -> Assert.state(handlers.add(handler),
() -> "Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
}

}
Loading