Skip to content

Commit 305686d

Browse files
committed
Ensure Bean Overrides are discovered once in @⁠Nested hierarchies
Changes made to the Bean Override search algorithms in commit 9181cce resulted in a regression that caused tests to start failing due to duplicate BeanOverrideHandlers under the following circumstances. - An enclosing class (typically a top-level test class) declares a @⁠BeanOverride such as @⁠MockitoBean. - An inner class is declared in that enclosing class. - A @⁠Nested test class which extends that inner class is declared in the same enclosing class. The reason for the duplicate detection is that the current search algorithm visits the common enclosing class twice. To address that, this commit revises the search algorithm in BeanOverrideHandler so that enclosing classes are only visited once. See gh-33925 Closes gh-34324
1 parent ace2f0a commit 305686d

File tree

3 files changed

+86
-6
lines changed

3 files changed

+86
-6
lines changed

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

+9-5
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
132132

133133
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
134134
List<BeanOverrideHandler> handlers = new ArrayList<>();
135-
findHandlers(testClass, testClass, handlers, localFieldsOnly);
135+
findHandlers(testClass, testClass, handlers, localFieldsOnly, new HashSet<>());
136136
return handlers;
137137
}
138138

@@ -146,26 +146,30 @@ private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolea
146146
* @param testClass the original test class
147147
* @param handlers the list of handlers found
148148
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
149+
* @param visitedEnclosingClasses the set of enclosing classes already visited
149150
* @since 6.2.2
150151
*/
151152
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
152-
boolean localFieldsOnly) {
153+
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
153154

154155
// 1) Search enclosing class hierarchy.
155156
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
156-
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
157+
Class<?> enclosingClass = clazz.getEnclosingClass();
158+
if (visitedEnclosingClasses.add(enclosingClass)) {
159+
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
160+
}
157161
}
158162

159163
// 2) Search class hierarchy.
160164
Class<?> superclass = clazz.getSuperclass();
161165
if (superclass != null && superclass != Object.class) {
162-
findHandlers(superclass, testClass, handlers, localFieldsOnly);
166+
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
163167
}
164168

165169
if (!localFieldsOnly) {
166170
// 3) Search interfaces.
167171
for (Class<?> ifc : clazz.getInterfaces()) {
168-
findHandlers(ifc, testClass, handlers, localFieldsOnly);
172+
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
169173
}
170174

171175
// 4) Process current class.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
import org.junit.jupiter.api.extension.ExtendWith;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.ApplicationContext;
25+
import org.springframework.test.context.bean.override.example.ExampleService;
26+
import org.springframework.test.context.junit.jupiter.SpringExtension;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
30+
31+
/**
32+
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
33+
* {@code @MockitoBean} fields are not discovered more than once when searching
34+
* intertwined enclosing class hierarchies and type hierarchies.
35+
*
36+
* @author Sam Brannen
37+
* @since 6.2.3
38+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34324">gh-34324</a>
39+
*/
40+
@ExtendWith(SpringExtension.class)
41+
class MockitoBeanNestedAndTypeHierarchiesTests {
42+
43+
@Autowired
44+
ApplicationContext enclosingContext;
45+
46+
@MockitoBean
47+
ExampleService service;
48+
49+
50+
@Test
51+
void topLevelTest() {
52+
assertIsMock(service);
53+
54+
// The following are prerequisites for the reported regression.
55+
assertThat(NestedTests.class.getSuperclass())
56+
.isEqualTo(AbstractBaseClassForNestedTests.class);
57+
assertThat(NestedTests.class.getEnclosingClass())
58+
.isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass())
59+
.isEqualTo(getClass());
60+
}
61+
62+
63+
abstract class AbstractBaseClassForNestedTests {
64+
65+
@Test
66+
void nestedTest(ApplicationContext nestedContext) {
67+
assertIsMock(service);
68+
assertThat(enclosingContext).isSameAs(nestedContext);
69+
}
70+
}
71+
72+
@Nested
73+
class NestedTests extends AbstractBaseClassForNestedTests {
74+
}
75+
76+
}

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ void checkMocks() {
9090

9191

9292
@MockitoBean(types = Service09.class)
93-
static class BaseTestCase implements TestInterface08 {
93+
class BaseTestCase implements TestInterface08 {
9494

9595
@Autowired
9696
Service08 service08;

0 commit comments

Comments
 (0)