diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
index 27d1265071c..32db0408cb0 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
@@ -331,6 +331,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return beanNames;
}
+ /**
+ * Determine the primary candidate in the given set of bean names.
+ *
Honors both primary and fallback semantics.
+ * @return the name of the primary candidate, or {@code null} if none found
+ * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate(Map, Class)
+ */
@Nullable
private static String determinePrimaryCandidate(
ConfigurableListableBeanFactory beanFactory, Set candidateBeanNames, Class> beanType) {
@@ -340,6 +346,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
String primaryBeanName = null;
+ // First pass: identify unique primary candidate
for (String candidateBeanName : candidateBeanNames) {
if (beanFactory.containsBeanDefinition(candidateBeanName)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
@@ -352,6 +359,21 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
}
}
+ // Second pass: identify unique non-fallback candidate
+ if (primaryBeanName == null) {
+ for (String candidateBeanName : candidateBeanNames) {
+ if (beanFactory.containsBeanDefinition(candidateBeanName)) {
+ BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
+ if (!beanDefinition.isFallback()) {
+ if (primaryBeanName != null) {
+ // More than one non-fallback bean found among candidates.
+ return null;
+ }
+ primaryBeanName = candidateBeanName;
+ }
+ }
+ }
+ }
return primaryBeanName;
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java
new file mode 100644
index 00000000000..037ea0d5291
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2024 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.convention;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Fallback;
+import org.springframework.test.annotation.DirtiesContext;
+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;
+
+/**
+ * Verifies that {@link TestBean @TestBean} can be used to override a bean by-type
+ * when there are multiple candidates and only one that is not a fallback.
+ *
+ * @author Sam Brannen
+ * @since 6.2.1
+ */
+@ExtendWith(SpringExtension.class)
+@DirtiesContext
+class TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests {
+
+ @TestBean
+ ExampleService service;
+
+ @Autowired
+ List services;
+
+
+ static ExampleService service() {
+ return () -> "overridden";
+ }
+
+
+ @Test
+ void test() {
+ assertThat(service.greeting()).isEqualTo("overridden");
+ assertThat(services).extracting(ExampleService::greeting)
+ .containsExactlyInAnyOrder("overridden", "two", "three");
+ }
+
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ ExampleService one() {
+ return () -> "one";
+ }
+
+ @Bean
+ @Fallback
+ ExampleService two() {
+ return () -> "two";
+ }
+
+ @Bean
+ @Fallback
+ ExampleService three() {
+ return () -> "three";
+ }
+ }
+
+}