Skip to content

Commit e8f873a

Browse files
committed
Ensure Bean Overrides are discovered once in hierarchies
Prior to this commit, bean overrides (such as @⁠MockitoBean, etc.) were discovered multiple times if they were declared: - at the type-level on an interface that is implemented at more than one level in the type hierarchy, the enclosing class hierarchy, or a combination of the type and enclosing class hierarchies. or - on a field declared in a class which can be reached multiple times while traversing the type and enclosing class hierarchies in scenarios such as the following: the class (X) in which the field is declared is a supertype of an enclosing type of the test class, and X is also an enclosing type of a supertype of the test class. Such scenarios resulted in an IllegalStateException stating that a duplicate BeanOverrideHandler was discovered. To address that, this commit revises the search algorithm in BeanOverrideHandler so that all types (superclasses, enclosing classes, and implemented interfaces) are only visited once while traversing the type and enclosing class hierarchies in search of bean override handlers. See gh-33925 See gh-34324 Closes gh-34844
1 parent b943817 commit e8f873a

5 files changed

+211
-10
lines changed

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,30 +184,32 @@ private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolea
184184
* @param testClass the original test class
185185
* @param handlers the list of handlers found
186186
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
187-
* @param visitedEnclosingClasses the set of enclosing classes already visited
187+
* @param visitedTypes the set of types already visited
188188
* @since 6.2.2
189189
*/
190190
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
191-
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
191+
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
192+
193+
// 0) Ensure that we do not process the same class or interface multiple times.
194+
if (!visitedTypes.add(clazz)) {
195+
return;
196+
}
192197

193198
// 1) Search enclosing class hierarchy.
194199
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
195-
Class<?> enclosingClass = clazz.getEnclosingClass();
196-
if (visitedEnclosingClasses.add(enclosingClass)) {
197-
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
198-
}
200+
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
199201
}
200202

201203
// 2) Search class hierarchy.
202204
Class<?> superclass = clazz.getSuperclass();
203205
if (superclass != null && superclass != Object.class) {
204-
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
206+
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
205207
}
206208

207209
if (!localFieldsOnly) {
208210
// 3) Search interfaces.
209211
for (Class<?> ifc : clazz.getInterfaces()) {
210-
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
212+
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
211213
}
212214

213215
// 4) Process current class.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.ApplicationContext;
24+
import org.springframework.test.context.bean.override.example.ExampleService;
25+
import org.springframework.test.context.junit.jupiter.SpringExtension;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
29+
30+
/**
31+
* Abstract top-level class and abstract inner class for integration tests for
32+
* {@link MockitoBean @MockitoBean} which verify that {@code @MockitoBean} fields
33+
* are not discovered more than once when searching intertwined enclosing class
34+
* hierarchies and type hierarchies, when a superclass is <em>present</em> twice
35+
* in the intertwined hierarchies.
36+
*
37+
* @author Sam Brannen
38+
* @since 6.2.7
39+
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
40+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
41+
*/
42+
@ExtendWith(SpringExtension.class)
43+
abstract class AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
44+
45+
@Autowired
46+
ApplicationContext enclosingContext;
47+
48+
@MockitoBean
49+
ExampleService service;
50+
51+
52+
@Test
53+
void topLevelTest() {
54+
assertIsMock(service);
55+
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
56+
}
57+
58+
59+
abstract class AbstractBaseClassForNestedTests {
60+
61+
@Test
62+
void nestedTest(ApplicationContext nestedContext) {
63+
assertIsMock(service);
64+
assertThat(enclosingContext).isSameAs(nestedContext);
65+
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
66+
}
67+
}
68+
69+
}

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java renamed to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@
3131
/**
3232
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
3333
* {@code @MockitoBean} fields are not discovered more than once when searching
34-
* intertwined enclosing class hierarchies and type hierarchies.
34+
* intertwined enclosing class hierarchies and type hierarchies, when an enclosing
35+
* class is <em>present</em> twice in the intertwined hierarchies.
3536
*
3637
* @author Sam Brannen
3738
* @since 6.2.3
39+
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
40+
* @see MockitoBeanWithInterfacePresentTwiceTests
3841
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34324">gh-34324</a>
3942
*/
4043
@ExtendWith(SpringExtension.class)
41-
class MockitoBeanNestedAndTypeHierarchiesTests {
44+
class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
4245

4346
@Autowired
4447
ApplicationContext enclosingContext;
@@ -50,6 +53,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests {
5053
@Test
5154
void topLevelTest() {
5255
assertIsMock(service);
56+
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
5357

5458
// The following are prerequisites for the reported regression.
5559
assertThat(NestedTests.class.getSuperclass())
@@ -66,6 +70,7 @@ abstract class AbstractBaseClassForNestedTests {
6670
void nestedTest(ApplicationContext nestedContext) {
6771
assertIsMock(service);
6872
assertThat(enclosingContext).isSameAs(nestedContext);
73+
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
6974
}
7075
}
7176

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
/**
25+
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
26+
* {@code @MockitoBean} fields are not discovered more than once when searching
27+
* intertwined enclosing class hierarchies and type hierarchies, when a superclass
28+
* is <em>present</em> twice in the intertwined hierarchies.
29+
*
30+
* @author Sam Brannen
31+
* @since 6.2.7
32+
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
33+
* @see MockitoBeanWithInterfacePresentTwiceTests
34+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
35+
*/
36+
class MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
37+
extends AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
38+
39+
@Test
40+
@Override
41+
void topLevelTest() {
42+
super.topLevelTest();
43+
44+
// The following are prerequisites for the reported regression.
45+
assertThat(NestedTests.class.getSuperclass())
46+
.isEqualTo(AbstractBaseClassForNestedTests.class);
47+
assertThat(NestedTests.class.getEnclosingClass())
48+
.isEqualTo(getClass());
49+
assertThat(NestedTests.class.getEnclosingClass().getSuperclass())
50+
.isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass())
51+
.isEqualTo(getClass().getSuperclass());
52+
}
53+
54+
55+
@Nested
56+
class NestedTests extends AbstractBaseClassForNestedTests {
57+
}
58+
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.ApplicationContext;
24+
import org.springframework.test.context.bean.override.example.ExampleService;
25+
import org.springframework.test.context.junit.jupiter.SpringExtension;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
29+
30+
/**
31+
* Integration tests for {@link MockitoBean @MockitoBean} which verify that type-level
32+
* {@code @MockitoBean} declarations are not discovered more than once when searching
33+
* a type hierarchy, when an interface is <em>present</em> twice in the hierarchy.
34+
*
35+
* @author Sam Brannen
36+
* @since 6.2.7
37+
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
38+
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
39+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
40+
*/
41+
class MockitoBeanWithInterfacePresentTwiceTests extends AbstractMockitoBeanWithInterfacePresentTwiceTests
42+
implements MockConfigInterface {
43+
44+
@Test
45+
void test(ApplicationContext context) {
46+
assertIsMock(service);
47+
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
48+
49+
// The following are prerequisites for the tested scenario.
50+
assertThat(getClass().getInterfaces()).containsExactly(MockConfigInterface.class);
51+
assertThat(getClass().getSuperclass().getInterfaces()).containsExactly(MockConfigInterface.class);
52+
}
53+
54+
}
55+
56+
@MockitoBean(types = ExampleService.class)
57+
interface MockConfigInterface {
58+
}
59+
60+
@ExtendWith(SpringExtension.class)
61+
abstract class AbstractMockitoBeanWithInterfacePresentTwiceTests implements MockConfigInterface {
62+
63+
@Autowired
64+
ExampleService service;
65+
66+
}

0 commit comments

Comments
 (0)