Skip to content
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

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

Open
wants to merge 1 commit into
base: 6.2.x
Choose a base branch
from

Conversation

sbrannen
Copy link
Member

@sbrannen sbrannen commented Apr 6, 2025

Background

As explained by @wilkinsona in #33293 (comment), the Spring TestContext Framework (TCF) creates a MergedContextConfiguration for each ApplicationContext that it may create. Furthermore, this applies to every ApplicationContext created for each level in a @ContextHierarchy.

Thus, by default, all context configuration mechanisms for a given test class will be applied to all application contexts created for that test class. Specifically, this means that features such as @ActiveProfiles, @TestPropertySource, and bean overrides (@TestBean, @MockitoBean, @MockitoSpyBean, etc.) will be applied to each application context.

For features such as @ActiveProfiles and @TestPropertySource, that makes sense. You probably always want the same bean definition profiles and test property sources to be applied to every application context in a context hierarchy.

However, with bean overrides that is typically not the case. Rather, when you configure a bean override, you expect Spring to override a specific bean in a specific application context in the context hierarchy. Phrased differently, you probably do not want Spring to attempt to override matching beans in all application contexts in a context hierarchy.

If Spring does attempt to override matching beans in all application contexts in a context hierarchy, that can lead to unexpected behavior or errors like those reported in #33293 and #34597.

Proposal

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

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

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

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

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

    // ...
}

The above example demonstrates how this feature would help to resolve the issue reported in #34597.

A similar approach can be applied to the failing use case reported in #33293, and the ReusedParentConfigV1Tests and ReusedParentConfigV2Tests test classes in this PR demonstrate that.

@Autowired with Magical Powers

The proposed feature also introduces functionality that is not possible with @Autowired. @Autowired will always inject a matching bean found in the lowest level of the context hierarchy. However, with bean overrides in different levels of the context hierarchy, you may need to be able to have all of those bean override instances injected into the test class in order to interact with them -- for example, to configure stubbing for a mock.

The following example demonstrates that is possible to inject an ExampleService mock from both the "parent" and the "child" context simultaneously.

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

    @MockitoBean(contextName = "parent")
    ExampleService serviceInParent;

    @MockitoBean(contextName = "child")
    ExampleService serviceInChild;

    // ...
}

Ramifications

In order to support reliable context caching and field injection, the contextName attribute contributes to MergedContextConfiguration cache key. Thus, bean overrides that would otherwise appear to be logically equivalent are not considered equivalent if they specify different context names. Consequently, specifying different context names can result in an application context being loaded multiple times.

Related Issues

@sbrannen sbrannen added in: test Issues in the test module type: enhancement A general enhancement labels Apr 6, 2025
@sbrannen sbrannen self-assigned this Apr 6, 2025
@sbrannen sbrannen force-pushed the issues/bean-overrides-in-context-hierarchy branch from 0eea7ef to 52b74ad Compare April 7, 2025 08:56
@sbrannen sbrannen force-pushed the issues/bean-overrides-in-context-hierarchy branch 2 times, most recently from 9d41612 to 2fdf133 Compare April 7, 2025 13:58
This commit provides first-class support for Bean Overrides
(@⁠MockitoBean, @⁠MockitoSpyBean, @⁠TestBean, etc.) with
@⁠ContextHierarchy.

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

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

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

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

    // ...
}

See spring-projectsgh-33293
See spring-projectsgh-34597
See spring-projectsgh-34726

Signed-off-by: Sam Brannen <[email protected]>
@sbrannen sbrannen force-pushed the issues/bean-overrides-in-context-hierarchy branch from 2fdf133 to 1bdd3a9 Compare April 7, 2025 14:54
@sbrannen sbrannen marked this pull request as ready for review April 7, 2025 16:06
@sbrannen sbrannen requested review from snicoll and removed request for snicoll April 7, 2025 16:07
@sbrannen sbrannen added this to the 6.2.6 milestone Apr 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test Issues in the test module type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant