Browse Source

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
pull/35405/head
Sam Brannen 8 months ago
parent
commit
e8f873a349
  1. 18
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
  2. 69
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java
  3. 9
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java
  4. 59
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java
  5. 66
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithInterfacePresentTwiceTests.java

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

@ -184,30 +184,32 @@ public abstract class BeanOverrideHandler { @@ -184,30 +184,32 @@ public abstract class BeanOverrideHandler {
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @param visitedEnclosingClasses the set of enclosing classes already visited
* @param visitedTypes the set of types already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
// 0) Ensure that we do not process the same class or interface multiple times.
if (!visitedTypes.add(clazz)) {
return;
}
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
}
// 4) Process current class.

69
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Abstract top-level class and abstract inner class for integration tests for
* {@link MockitoBean @MockitoBean} which verify that {@code @MockitoBean} fields
* are not discovered more than once when searching intertwined enclosing class
* hierarchies and type hierarchies, when a superclass is <em>present</em> twice
* in the intertwined hierarchies.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
@ExtendWith(SpringExtension.class)
abstract class AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
@Autowired
ApplicationContext enclosingContext;
@MockitoBean
ExampleService service;
@Test
void topLevelTest() {
assertIsMock(service);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
abstract class AbstractBaseClassForNestedTests {
@Test
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
}
}

9
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java → spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java

@ -31,14 +31,17 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; @@ -31,14 +31,17 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
* {@code @MockitoBean} fields are not discovered more than once when searching
* intertwined enclosing class hierarchies and type hierarchies.
* intertwined enclosing class hierarchies and type hierarchies, when an enclosing
* class is <em>present</em> twice in the intertwined hierarchies.
*
* @author Sam Brannen
* @since 6.2.3
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see MockitoBeanWithInterfacePresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34324">gh-34324</a>
*/
@ExtendWith(SpringExtension.class)
class MockitoBeanNestedAndTypeHierarchiesTests {
class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
@Autowired
ApplicationContext enclosingContext;
@ -50,6 +53,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests { @@ -50,6 +53,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests {
@Test
void topLevelTest() {
assertIsMock(service);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
@ -66,6 +70,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests { @@ -66,6 +70,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests {
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
}

59
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
* {@code @MockitoBean} fields are not discovered more than once when searching
* intertwined enclosing class hierarchies and type hierarchies, when a superclass
* is <em>present</em> twice in the intertwined hierarchies.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
* @see MockitoBeanWithInterfacePresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
class MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
extends AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
@Test
@Override
void topLevelTest() {
super.topLevelTest();
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
.isEqualTo(AbstractBaseClassForNestedTests.class);
assertThat(NestedTests.class.getEnclosingClass())
.isEqualTo(getClass());
assertThat(NestedTests.class.getEnclosingClass().getSuperclass())
.isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass())
.isEqualTo(getClass().getSuperclass());
}
@Nested
class NestedTests extends AbstractBaseClassForNestedTests {
}
}

66
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithInterfacePresentTwiceTests.java

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that type-level
* {@code @MockitoBean} declarations are not discovered more than once when searching
* a type hierarchy, when an interface is <em>present</em> twice in the hierarchy.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
class MockitoBeanWithInterfacePresentTwiceTests extends AbstractMockitoBeanWithInterfacePresentTwiceTests
implements MockConfigInterface {
@Test
void test(ApplicationContext context) {
assertIsMock(service);
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
// The following are prerequisites for the tested scenario.
assertThat(getClass().getInterfaces()).containsExactly(MockConfigInterface.class);
assertThat(getClass().getSuperclass().getInterfaces()).containsExactly(MockConfigInterface.class);
}
}
@MockitoBean(types = ExampleService.class)
interface MockConfigInterface {
}
@ExtendWith(SpringExtension.class)
abstract class AbstractMockitoBeanWithInterfacePresentTwiceTests implements MockConfigInterface {
@Autowired
ExampleService service;
}
Loading…
Cancel
Save