diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java index b1108139200..fdfc718385a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * 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. @@ -103,10 +103,27 @@ public abstract class BeanOverrideHandler { */ public static List forTestClass(Class testClass) { List handlers = new LinkedList<>(); - ReflectionUtils.doWithFields(testClass, field -> processField(field, testClass, handlers)); + findHandlers(testClass, testClass, handlers); return handlers; } + /** + * Find handlers using tail recursion to ensure that "locally declared" + * bean overrides take precedence over inherited bean overrides. + * @since 6.2.2 + */ + private static void findHandlers(Class clazz, Class testClass, List handlers) { + if (clazz == null || clazz == Object.class) { + return; + } + + // 1) Search type hierarchy. + findHandlers(clazz.getSuperclass(), testClass, handlers); + + // 2) Process fields in current class. + ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers)); + } + private static void processField(Field field, Class testClass, List handlers) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForInheritanceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java similarity index 70% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForInheritanceIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java index 5185045b4cb..9e3890c005b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForInheritanceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java @@ -38,14 +38,21 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 6.2 */ -public class TestBeanForInheritanceIntegrationTests { +@SpringJUnitConfig +public class TestBeanInheritanceIntegrationTests { + + @TestBean + Pojo puzzleBean; + + static Pojo puzzleBean() { + return new FakePojo("puzzle in enclosing class"); + } static Pojo enclosingClassBean() { return new FakePojo("in enclosing test class"); } - @SpringJUnitConfig - abstract static class AbstractTestBeanIntegrationTestCase { + abstract static class AbstractTestCase { @TestBean Pojo someBean; @@ -56,6 +63,9 @@ public class TestBeanForInheritanceIntegrationTests { @TestBean("thirdBean") Pojo anotherBean; + @TestBean + Pojo enigmaBean; + static Pojo otherBean() { return new FakePojo("other in superclass"); } @@ -64,44 +74,18 @@ public class TestBeanForInheritanceIntegrationTests { return new FakePojo("third in superclass"); } + static Pojo enigmaBean() { + return new FakePojo("enigma in superclass"); + } + static Pojo commonBean() { return new FakePojo("common in superclass"); } - - @Configuration(proxyBeanMethods = false) - static class Config { - - @Bean - Pojo someBean() { - return new ProdPojo(); - } - - @Bean - Pojo otherBean() { - return new ProdPojo(); - } - - @Bean - Pojo thirdBean() { - return new ProdPojo(); - } - - @Bean - Pojo pojo() { - return new ProdPojo(); - } - - @Bean - Pojo pojo2() { - return new ProdPojo(); - } - } - } @Nested @DisplayName("Nested, concrete inherited tests with correct @TestBean setup") - class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase { + class NestedTests extends AbstractTestCase { @Autowired ApplicationContext ctx; @@ -112,6 +96,21 @@ public class TestBeanForInheritanceIntegrationTests { @TestBean(name = "pojo2", methodName = "enclosingClassBean") Pojo pojo2; + @TestBean(methodName = "localEnigmaBean") + Pojo enigmaBean; + + @TestBean + Pojo puzzleBean; + + + static Pojo puzzleBean() { + return new FakePojo("puzzle in nested class"); + } + + static Pojo localEnigmaBean() { + return new FakePojo("enigma in subclass"); + } + static Pojo someBean() { return new FakePojo("someBeanOverride"); } @@ -150,6 +149,57 @@ public class TestBeanForInheritanceIntegrationTests { assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class"); assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class"); } + + @Test // gh-34194 + void testBeanInSubclassOverridesTestBeanInSuperclass() { + assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass"); + assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass"); + } + + @Test // gh-34194 + void testBeanInNestedClassOverridesTestBeanInEnclosingClass() { + assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class"); + assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class"); + } + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + Pojo someBean() { + return new ProdPojo(); + } + + @Bean + Pojo otherBean() { + return new ProdPojo(); + } + + @Bean + Pojo thirdBean() { + return new ProdPojo(); + } + + @Bean + Pojo enigmaBean() { + return new ProdPojo(); + } + + @Bean + Pojo puzzleBean() { + return new ProdPojo(); + } + + @Bean + Pojo pojo() { + return new ProdPojo(); + } + + @Bean + Pojo pojo2() { + return new ProdPojo(); + } } interface Pojo {