Skip to content

Commit 0eea7ef

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
1 parent 470bf3b commit 0eea7ef

File tree

40 files changed

+2356
-47
lines changed

40 files changed

+2356
-47
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

+30-6
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,23 @@
2626
import org.apache.commons.logging.LogFactory;
2727

2828
import org.springframework.beans.factory.BeanCreationException;
29+
import org.springframework.beans.factory.BeanFactory;
2930
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
31+
import org.springframework.lang.Nullable;
3032
import org.springframework.util.Assert;
3133
import org.springframework.util.ReflectionUtils;
32-
import org.springframework.util.StringUtils;
34+
35+
import static org.springframework.test.context.bean.override.BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME;
3336

3437
/**
3538
* An internal class used to track {@link BeanOverrideHandler}-related state after
3639
* the bean factory has been processed and to provide field injection utilities
3740
* for test execution listeners.
3841
*
42+
* <p>As of Spring Framework 6.2.6, {@code BeanOverrideRegistry} is hierarchical
43+
* and has access to a potential parent in order to provide first-class support
44+
* for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}.
45+
*
3946
* @author Simon Baslé
4047
* @author Sam Brannen
4148
* @since 6.2
@@ -51,10 +58,16 @@ class BeanOverrideRegistry {
5158

5259
private final ConfigurableBeanFactory beanFactory;
5360

61+
@Nullable
62+
private final BeanOverrideRegistry parent;
63+
5464

5565
BeanOverrideRegistry(ConfigurableBeanFactory beanFactory) {
5666
Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null");
5767
this.beanFactory = beanFactory;
68+
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
69+
this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(REGISTRY_BEAN_NAME) ?
70+
parentBeanFactory.getBean(REGISTRY_BEAN_NAME, BeanOverrideRegistry.class) : null);
5871
}
5972

6073
/**
@@ -110,14 +123,13 @@ Object wrapBeanIfNecessary(Object bean, String beanName) {
110123
void inject(Object target, BeanOverrideHandler handler) {
111124
Field field = handler.getField();
112125
Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler);
113-
String beanName = this.handlerToBeanNameMap.get(handler);
114-
Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler);
115-
inject(field, target, beanName);
126+
Object bean = getBeanForHandler(handler, field.getType());
127+
Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler);
128+
inject(field, target, bean);
116129
}
117130

118-
private void inject(Field field, Object target, String beanName) {
131+
private void inject(Field field, Object target, Object bean) {
119132
try {
120-
Object bean = this.beanFactory.getBean(beanName, field.getType());
121133
ReflectionUtils.makeAccessible(field);
122134
ReflectionUtils.setField(field, target, bean);
123135
}
@@ -126,4 +138,16 @@ private void inject(Field field, Object target, String beanName) {
126138
}
127139
}
128140

141+
@Nullable
142+
private Object getBeanForHandler(BeanOverrideHandler handler, Class<?> requiredType) {
143+
String beanName = this.handlerToBeanNameMap.get(handler);
144+
if (beanName != null) {
145+
return this.beanFactory.getBean(beanName, requiredType);
146+
}
147+
if (this.parent != null) {
148+
return this.parent.getBeanForHandler(handler, requiredType);
149+
}
150+
return null;
151+
}
152+
129153
}

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

0 commit comments

Comments
 (0)