Skip to content

Commit 1bdd3a9

Browse files
committed
Provide first-class support for Bean Overrides with @⁠ContextHierarchy
This commit provides first-class support for Bean Overrides (@⁠MockitoBean, @⁠MockitoSpyBean, @⁠TestBean, etc.) with @⁠ContextHierarchy. Specifically, bean overrides can now specify which ApplicationContext they target within the context hierarchy by configuring the `contextName` attribute in the annotation. The `contextName` must match a corresponding `name` configured via @⁠ContextConfiguration. For example, the following test class configures the name of the second hierarchy level to be "child" and simultaneously specifies that the ExampleService should be wrapped in a Mockito spy in the context named "child". Consequently, Spring will only attempt to create the spy in the "child" context and will not attempt to create the spy in the parent context. @ExtendWith(SpringExtension.class) @ContextHierarchy({ @ContextConfiguration(classes = Config1.class), @ContextConfiguration(classes = Config2.class, name = "child") }) class MockitoSpyBeanContextHierarchyTests { @MockitoSpyBean(contextName = "child") ExampleService service; // ... } See gh-33293 See gh-34597 See gh-34726 Signed-off-by: Sam Brannen <[email protected]>
1 parent 4510b78 commit 1bdd3a9

File tree

41 files changed

+2486
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2486
-42
lines changed

spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java

+12-6
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
4242
public BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass,
4343
List<ContextConfigurationAttributes> configAttributes) {
4444

45+
// Base the context name on the "closest" @ContextConfiguration declaration
46+
// within the type and enclosing class hierarchies of the test class.
47+
String contextName = configAttributes.get(0).getName();
4548
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
46-
findBeanOverrideHandlers(testClass, handlers);
49+
findBeanOverrideHandlers(testClass, contextName, handlers);
4750
if (handlers.isEmpty()) {
4851
return null;
4952
}
5053
return new BeanOverrideContextCustomizer(handlers);
5154
}
5255

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

6066
}

spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java

+44-1
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,17 @@ public abstract class BeanOverrideHandler {
8787
@Nullable
8888
private final String beanName;
8989

90+
private final String contextName;
91+
9092
private final BeanOverrideStrategy strategy;
9193

9294

9395
/**
9496
* Construct a new {@code BeanOverrideHandler} from the supplied values.
97+
* <p>To provide proper support for
98+
* {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy},
99+
* invoke {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)}
100+
* instead.
95101
* @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride},
96102
* or {@code null} if {@code @BeanOverride} was declared at the type level
97103
* @param beanType the {@linkplain ResolvableType type} of bean to override
@@ -102,11 +108,31 @@ public abstract class BeanOverrideHandler {
102108
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
103109
BeanOverrideStrategy strategy) {
104110

111+
this(field, beanType, beanName, "", strategy);
112+
}
113+
114+
/**
115+
* Construct a new {@code BeanOverrideHandler} from the supplied values.
116+
* @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride},
117+
* or {@code null} if {@code @BeanOverride} was declared at the type level
118+
* @param beanType the {@linkplain ResolvableType type} of bean to override
119+
* @param beanName the name of the bean to override, or {@code null} to look
120+
* for a single matching bean by type
121+
* @param contextName the name of the context hierarchy level in which the
122+
* handler should be applied, or an empty string to indicate that the handler
123+
* should be applied to all application contexts within a context hierarchy
124+
* @param strategy the {@link BeanOverrideStrategy} to use
125+
* @since 6.2.6
126+
*/
127+
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
128+
String contextName, BeanOverrideStrategy strategy) {
129+
105130
this.field = field;
106131
this.qualifierAnnotations = getQualifierAnnotations(field);
107132
this.beanType = beanType;
108133
this.beanName = beanName;
109134
this.strategy = strategy;
135+
this.contextName = contextName;
110136
}
111137

112138
/**
@@ -247,6 +273,21 @@ public final String getBeanName() {
247273
return this.beanName;
248274
}
249275

276+
/**
277+
* Get the name of the context hierarchy level in which this handler should
278+
* be applied.
279+
* <p>An empty string indicates that this handler should be applied to all
280+
* application contexts within a context hierarchy.
281+
* <p>If a context name is configured for this handler, it must match a name
282+
* configured via {@code @ContextConfiguration(name=...)}.
283+
* @since 6.2.6
284+
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
285+
* @see org.springframework.test.context.ContextConfiguration#name()
286+
*/
287+
public final String getContextName() {
288+
return this.contextName;
289+
}
290+
250291
/**
251292
* Get the {@link BeanOverrideStrategy} for this {@code BeanOverrideHandler},
252293
* which influences how and when the bean override instance should be created.
@@ -320,6 +361,7 @@ public boolean equals(Object other) {
320361
BeanOverrideHandler that = (BeanOverrideHandler) other;
321362
if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) ||
322363
!Objects.equals(this.beanName, that.beanName) ||
364+
!Objects.equals(this.contextName, that.contextName) ||
323365
!Objects.equals(this.strategy, that.strategy)) {
324366
return false;
325367
}
@@ -339,7 +381,7 @@ public boolean equals(Object other) {
339381

340382
@Override
341383
public int hashCode() {
342-
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
384+
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.contextName, this.strategy);
343385
return (this.beanName != null ? hash : hash +
344386
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
345387
}
@@ -350,6 +392,7 @@ public String toString() {
350392
.append("field", this.field)
351393
.append("beanType", this.beanType)
352394
.append("beanName", this.beanName)
395+
.append("contextName", this.contextName)
353396
.append("strategy", this.strategy)
354397
.toString();
355398
}

spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@
2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
2626

27+
import org.springframework.beans.factory.BeanFactory;
2728
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2829
import org.springframework.lang.Nullable;
2930
import org.springframework.util.Assert;
3031

32+
import static org.springframework.test.context.bean.override.BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME;
33+
3134
/**
3235
* An internal class used to track {@link BeanOverrideHandler}-related state after
3336
* the bean factory has been processed and to provide lookup facilities to test
3437
* execution listeners.
3538
*
39+
* <p>As of Spring Framework 6.2.6, {@code BeanOverrideRegistry} is hierarchical
40+
* and has access to a potential parent in order to provide first-class support
41+
* for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}.
42+
*
3643
* @author Simon Baslé
3744
* @author Sam Brannen
3845
* @since 6.2
@@ -48,10 +55,16 @@ class BeanOverrideRegistry {
4855

4956
private final ConfigurableBeanFactory beanFactory;
5057

58+
@Nullable
59+
private final BeanOverrideRegistry parent;
60+
5161

5262
BeanOverrideRegistry(ConfigurableBeanFactory beanFactory) {
5363
Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null");
5464
this.beanFactory = beanFactory;
65+
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
66+
this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(REGISTRY_BEAN_NAME) ?
67+
parentBeanFactory.getBean(REGISTRY_BEAN_NAME, BeanOverrideRegistry.class) : null);
5568
}
5669

5770
/**
@@ -110,7 +123,7 @@ Object wrapBeanIfNecessary(Object bean, String beanName) {
110123
* @param handler the {@code BeanOverrideHandler} that created the bean
111124
* @param requiredType the required bean type
112125
* @return the bean instance, or {@code null} if the provided handler is not
113-
* registered in this registry
126+
* registered in this registry or a parent registry
114127
* @since 6.2.6
115128
* @see #registerBeanOverrideHandler(BeanOverrideHandler, String)
116129
*/
@@ -120,6 +133,9 @@ Object getBeanForHandler(BeanOverrideHandler handler, Class<?> requiredType) {
120133
if (beanName != null) {
121134
return this.beanFactory.getBean(beanName, requiredType);
122135
}
136+
if (this.parent != null) {
137+
return this.parent.getBeanForHandler(handler, requiredType);
138+
}
123139
return null;
124140
}
125141

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java

+13
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,19 @@
164164
*/
165165
String methodName() default "";
166166

167+
/**
168+
* The name of the context hierarchy level in which this {@code @TestBean}
169+
* should be applied.
170+
* <p>Defaults to an empty string which indicates that this {@code @TestBean}
171+
* should be applied to all application contexts within a context hierarchy.
172+
* <p>If a context name is configured, it must match a name configured via
173+
* {@code @ContextConfiguration(name=...)}.
174+
* @since 6.2.6
175+
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
176+
* @see org.springframework.test.context.ContextConfiguration#name()
177+
*/
178+
String contextName() default "";
179+
167180
/**
168181
* Whether to require the existence of the bean being overridden.
169182
* <p>Defaults to {@code false} which means that a bean will be created if a

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler {
4343

4444

4545
TestBeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
46-
BeanOverrideStrategy strategy, Method factoryMethod) {
46+
String contextName, BeanOverrideStrategy strategy, Method factoryMethod) {
4747

48-
super(field, beanType, beanName, strategy);
48+
super(field, beanType, beanName, contextName, strategy);
4949
this.factoryMethod = factoryMethod;
5050
}
5151

@@ -90,6 +90,7 @@ public String toString() {
9090
.append("field", getField())
9191
.append("beanType", getBeanType())
9292
.append("beanName", getBeanName())
93+
.append("contextName", getContextName())
9394
.append("strategy", getStrategy())
9495
.append("factoryMethod", this.factoryMethod)
9596
.toString();

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas
8282
}
8383

8484
return new TestBeanOverrideHandler(
85-
field, ResolvableType.forField(field, testClass), beanName, strategy, factoryMethod);
85+
field, ResolvableType.forField(field, testClass), beanName, testBean.contextName(), strategy, factoryMethod);
8686
}
8787

8888
/**

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
3939

4040

4141
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
42-
@Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset) {
42+
@Nullable String beanName, String contextName, BeanOverrideStrategy strategy,
43+
MockReset reset) {
4344

44-
super(field, beanType, beanName, strategy);
45+
super(field, beanType, beanName, contextName, strategy);
4546
this.reset = (reset != null ? reset : MockReset.AFTER);
4647
}
4748

@@ -92,6 +93,7 @@ public String toString() {
9293
.append("field", getField())
9394
.append("beanType", getBeanType())
9495
.append("beanName", getBeanName())
96+
.append("contextName", getContextName())
9597
.append("strategy", getStrategy())
9698
.append("reset", getReset())
9799
.toString();

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java

+13
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@
144144
*/
145145
Class<?>[] types() default {};
146146

147+
/**
148+
* The name of the context hierarchy level in which this {@code @MockitoBean}
149+
* should be applied.
150+
* <p>Defaults to an empty string which indicates that this {@code @MockitoBean}
151+
* should be applied to all application contexts within a context hierarchy.
152+
* <p>If a context name is configured, it must match a name configured via
153+
* {@code @ContextConfiguration(name=...)}.
154+
* @since 6.2.6
155+
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
156+
* @see org.springframework.test.context.ContextConfiguration#name()
157+
*/
158+
String contextName() default "";
159+
147160
/**
148161
* Extra interfaces that should also be declared by the mock.
149162
* <p>Defaults to none.

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
6363

6464
MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
6565
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
66-
(mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
67-
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
66+
mockitoBean.contextName(), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
67+
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
6868
}
6969

7070
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
71-
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, Answers answers,
72-
boolean serializable) {
71+
String contextName, BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces,
72+
Answers answers, boolean serializable) {
7373

74-
super(field, typeToMock, beanName, strategy, reset);
74+
super(field, typeToMock, beanName, contextName, strategy, reset);
7575
Assert.notNull(typeToMock, "'typeToMock' must not be null");
7676
this.extraInterfaces = asClassSet(extraInterfaces);
7777
this.answers = answers;
@@ -160,6 +160,7 @@ public String toString() {
160160
.append("field", getField())
161161
.append("beanType", getBeanType())
162162
.append("beanName", getBeanName())
163+
.append("contextName", getContextName())
163164
.append("strategy", getStrategy())
164165
.append("reset", getReset())
165166
.append("extraInterfaces", getExtraInterfaces())

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java

+13
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@
136136
*/
137137
Class<?>[] types() default {};
138138

139+
/**
140+
* The name of the context hierarchy level in which this {@code @MockitoSpyBean}
141+
* should be applied.
142+
* <p>Defaults to an empty string which indicates that this {@code @MockitoSpyBean}
143+
* should be applied to all application contexts within a context hierarchy.
144+
* <p>If a context name is configured, it must match a name configured via
145+
* {@code @ContextConfiguration(name=...)}.
146+
* @since 6.2.6
147+
* @see org.springframework.test.context.ContextHierarchy @ContextHierarchy
148+
* @see org.springframework.test.context.ContextConfiguration#name()
149+
*/
150+
String contextName() default "";
151+
139152
/**
140153
* The reset mode to apply to the spied bean.
141154
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
5454

5555
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
5656
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
57-
BeanOverrideStrategy.WRAP, spyBean.reset());
57+
spyBean.contextName(), BeanOverrideStrategy.WRAP, spyBean.reset());
5858
Assert.notNull(typeToSpy, "typeToSpy must not be null");
5959
}
6060

spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,13 @@
1616

1717
package org.springframework.test.context.bean.override;
1818

19-
import java.util.Collections;
19+
import java.util.List;
2020
import java.util.function.Consumer;
2121

2222
import org.junit.jupiter.api.Test;
2323

2424
import org.springframework.lang.Nullable;
25+
import org.springframework.test.context.ContextConfigurationAttributes;
2526
import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler;
2627

2728
import static org.assertj.core.api.Assertions.assertThat;
@@ -92,7 +93,7 @@ private Consumer<BeanOverrideHandler> dummyHandler(@Nullable String beanName, Cl
9293

9394
@Nullable
9495
private BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass) {
95-
return this.factory.createContextCustomizer(testClass, Collections.emptyList());
96+
return this.factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass)));
9697
}
9798

9899

spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,11 @@
1616

1717
package org.springframework.test.context.bean.override;
1818

19-
import java.util.Collections;
19+
import java.util.List;
2020

2121
import org.springframework.context.ConfigurableApplicationContext;
2222
import org.springframework.lang.Nullable;
23+
import org.springframework.test.context.ContextConfigurationAttributes;
2324
import org.springframework.test.context.ContextCustomizer;
2425
import org.springframework.test.context.MergedContextConfiguration;
2526

@@ -44,7 +45,7 @@ public abstract class BeanOverrideContextCustomizerTestUtils {
4445
*/
4546
@Nullable
4647
public static ContextCustomizer createContextCustomizer(Class<?> testClass) {
47-
return factory.createContextCustomizer(testClass, Collections.emptyList());
48+
return factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass)));
4849
}
4950

5051
/**

0 commit comments

Comments
 (0)