Browse Source

Ensure Bean Overrides are discovered once in @⁠Nested hierarchies

Changes made to the Bean Override search algorithms in commit
9181cce65f 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
pull/34006/head
Sam Brannen 11 months ago
parent
commit
305686dbf7
  1. 14
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
  2. 76
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java
  3. 2
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java

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

@ -132,7 +132,7 @@ public abstract class BeanOverrideHandler { @@ -132,7 +132,7 @@ public abstract class BeanOverrideHandler {
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
List<BeanOverrideHandler> handlers = new ArrayList<>();
findHandlers(testClass, testClass, handlers, localFieldsOnly);
findHandlers(testClass, testClass, handlers, localFieldsOnly, new HashSet<>());
return handlers;
}
@ -146,26 +146,30 @@ public abstract class BeanOverrideHandler { @@ -146,26 +146,30 @@ 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
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly) {
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
// 4) Process current class.

76
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* 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 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
* {@code @MockitoBean} fields are not discovered more than once when searching
* intertwined enclosing class hierarchies and type hierarchies.
*
* @author Sam Brannen
* @since 6.2.3
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34324">gh-34324</a>
*/
@ExtendWith(SpringExtension.class)
class MockitoBeanNestedAndTypeHierarchiesTests {
@Autowired
ApplicationContext enclosingContext;
@MockitoBean
ExampleService service;
@Test
void topLevelTest() {
assertIsMock(service);
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
.isEqualTo(AbstractBaseClassForNestedTests.class);
assertThat(NestedTests.class.getEnclosingClass())
.isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass())
.isEqualTo(getClass());
}
abstract class AbstractBaseClassForNestedTests {
@Test
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
}
}
@Nested
class NestedTests extends AbstractBaseClassForNestedTests {
}
}

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

@ -90,7 +90,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 { @@ -90,7 +90,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@MockitoBean(types = Service09.class)
static class BaseTestCase implements TestInterface08 {
class BaseTestCase implements TestInterface08 {
@Autowired
Service08 service08;

Loading…
Cancel
Save