Merge branch '6.2.x'

This commit is contained in:
Sam Brannen
2025-01-05 12:18:52 +02:00
4 changed files with 107 additions and 40 deletions

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ import org.springframework.test.context.MergedContextConfiguration;
/** /**
* {@link ContextCustomizer} implementation that registers the necessary * {@link ContextCustomizer} implementation that registers the necessary
* infrastructure to support {@linkplain BeanOverride bean overriding}. * infrastructure to support {@linkplain BeanOverride Bean Overrides}.
* *
* @author Simon Baslé * @author Simon Baslé
* @author Stephane Nicoll * @author Stephane Nicoll

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.springframework.util.Assert;
/** /**
* {@link ContextCustomizerFactory} implementation that provides support for * {@link ContextCustomizerFactory} implementation that provides support for
* Bean Overriding. * {@linkplain BeanOverride Bean Overrides}.
* *
* @author Simon Baslé * @author Simon Baslé
* @author Stephane Nicoll * @author Stephane Nicoll

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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<BeanOverrideHandler> forTestClass(Class<?> testClass) { public static List<BeanOverrideHandler> forTestClass(Class<?> testClass) {
List<BeanOverrideHandler> handlers = new LinkedList<>(); List<BeanOverrideHandler> handlers = new LinkedList<>();
ReflectionUtils.doWithFields(testClass, field -> processField(field, testClass, handlers)); findHandlers(testClass, testClass, handlers);
return 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<BeanOverrideHandler> 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<BeanOverrideHandler> handlers) { private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {

View File

@@ -38,14 +38,21 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sam Brannen * @author Sam Brannen
* @since 6.2 * @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() { static Pojo enclosingClassBean() {
return new FakePojo("in enclosing test class"); return new FakePojo("in enclosing test class");
} }
@SpringJUnitConfig abstract static class AbstractTestCase {
abstract static class AbstractTestBeanIntegrationTestCase {
@TestBean @TestBean
Pojo someBean; Pojo someBean;
@@ -56,6 +63,9 @@ public class TestBeanForInheritanceIntegrationTests {
@TestBean("thirdBean") @TestBean("thirdBean")
Pojo anotherBean; Pojo anotherBean;
@TestBean
Pojo enigmaBean;
static Pojo otherBean() { static Pojo otherBean() {
return new FakePojo("other in superclass"); return new FakePojo("other in superclass");
} }
@@ -64,44 +74,18 @@ public class TestBeanForInheritanceIntegrationTests {
return new FakePojo("third in superclass"); return new FakePojo("third in superclass");
} }
static Pojo enigmaBean() {
return new FakePojo("enigma in superclass");
}
static Pojo commonBean() { static Pojo commonBean() {
return new FakePojo("common in superclass"); 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 @Nested
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup") @DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase { class NestedTests extends AbstractTestCase {
@Autowired @Autowired
ApplicationContext ctx; ApplicationContext ctx;
@@ -112,6 +96,21 @@ public class TestBeanForInheritanceIntegrationTests {
@TestBean(name = "pojo2", methodName = "enclosingClassBean") @TestBean(name = "pojo2", methodName = "enclosingClassBean")
Pojo pojo2; 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() { static Pojo someBean() {
return new FakePojo("someBeanOverride"); return new FakePojo("someBeanOverride");
} }
@@ -150,6 +149,57 @@ public class TestBeanForInheritanceIntegrationTests {
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class"); assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
assertThat(this.pojo2.value()).as("injection point").isEqualTo("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 { interface Pojo {