Skip to content

Commit 536de9a

Browse files
committed
Allow MockitoBean to create a mock for a bean that does not exist
This commit aligns the by-type lookup for bean overrides with what was done when a bean name is present. It now correctly generate a bean name rather than failing because no bean of that type exists. Closes gh-32990
1 parent bc98410 commit 536de9a

File tree

4 files changed

+54
-43
lines changed

4 files changed

+54
-43
lines changed

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

+33-12
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.beans.factory.config.DependencyDescriptor;
3535
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
3636
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
37+
import org.springframework.beans.factory.support.BeanNameGenerator;
38+
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
3739
import org.springframework.beans.factory.support.RootBeanDefinition;
3840
import org.springframework.core.Ordered;
3941
import org.springframework.core.PriorityOrdered;
@@ -63,6 +65,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
6365

6466
private final BeanOverrideRegistrar overrideRegistrar;
6567

68+
private final BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
69+
6670

6771
/**
6872
* Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with
@@ -133,18 +137,11 @@ private void registerReplaceDefinition(ConfigurableListableBeanFactory beanFacto
133137
String beanNameIncludingFactory;
134138
BeanDefinition existingBeanDefinition = null;
135139
if (beanName == null) {
136-
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
137-
int candidateCount = candidateNames.size();
138-
if (candidateCount != 1) {
139-
Field field = overrideMetadata.getField();
140-
throw new IllegalStateException("Unable to select a bean definition to override: found " +
141-
candidateCount + " bean definitions of type " + overrideMetadata.getBeanType() +
142-
" (as required by annotated field '" + field.getDeclaringClass().getSimpleName() +
143-
"." + field.getName() + "')" + (candidateCount > 0 ? ": " + candidateNames : ""));
144-
}
145-
beanNameIncludingFactory = candidateNames.iterator().next();
140+
beanNameIncludingFactory = getBeanNameForType(beanFactory, registry, overrideMetadata, beanDefinition, enforceExistingDefinition);
146141
beanName = BeanFactoryUtils.transformedBeanName(beanNameIncludingFactory);
147-
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
142+
if (registry.containsBeanDefinition(beanName)) {
143+
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
144+
}
148145
}
149146
else {
150147
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
@@ -176,6 +173,30 @@ else if (enforceExistingDefinition) {
176173
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory);
177174
}
178175

176+
private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
177+
OverrideMetadata overrideMetadata, RootBeanDefinition beanDefinition, boolean enforceExistingDefinition) {
178+
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
179+
int candidateCount = candidateNames.size();
180+
if (candidateCount == 1) {
181+
return candidateNames.iterator().next();
182+
}
183+
else if (candidateCount == 0) {
184+
if (enforceExistingDefinition) {
185+
Field field = overrideMetadata.getField();
186+
throw new IllegalStateException(
187+
"Unable to override bean: no bean definitions of type %s (as required by annotated field '%s.%s')"
188+
.formatted(overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(), field.getName()));
189+
}
190+
return this.beanNameGenerator.generateBeanName(beanDefinition, registry);
191+
}
192+
Field field = overrideMetadata.getField();
193+
throw new IllegalStateException(String.format(
194+
"Unable to select a bean definition to override: found %s bean definitions of type %s " +
195+
"(as required by annotated field '%s.%s'): %s",
196+
candidateCount, overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(),
197+
field.getName(), candidateNames));
198+
}
199+
179200
/**
180201
* Check that the expected bean name is registered and matches the type to override.
181202
* <p>If so, put the override metadata in the early tracking map.
@@ -209,7 +230,7 @@ private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, Overr
209230
}
210231

211232
RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
212-
RootBeanDefinition definition = new RootBeanDefinition();
233+
RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve());
213234
definition.setTargetType(metadata.getBeanType());
214235
definition.setQualifiedElement(metadata.getField());
215236
return definition;

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/FailingTestBeanByTypeIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ void zeroCandidates() {
5151
cause(
5252
instanceOf(IllegalStateException.class),
5353
message("""
54-
Unable to select a bean definition to override: found 0 bean definitions \
55-
of type %s (as required by annotated field '%s.example')"""
54+
Unable to override bean: no bean definitions of type \
55+
%s (as required by annotated field '%s.example')"""
5656
.formatted(ExampleService.class.getName(), testClass.getSimpleName())))));
5757
}
5858

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

-29
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,6 @@
4343
*/
4444
class FailingMockitoBeanByTypeIntegrationTests {
4545

46-
@Test
47-
void zeroCandidates() {
48-
Class<?> testClass = ZeroCandidatesTestCase.class;
49-
EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1,
50-
finishedWithFailure(
51-
cause(
52-
instanceOf(IllegalStateException.class),
53-
message("""
54-
Unable to select a bean definition to override: found 0 bean definitions \
55-
of type %s (as required by annotated field '%s.example')"""
56-
.formatted(ExampleService.class.getName(), testClass.getSimpleName())))));
57-
}
58-
5946
@Test
6047
void tooManyCandidates() {
6148
Class<?> testClass = TooManyCandidatesTestCase.class;
@@ -70,22 +57,6 @@ void tooManyCandidates() {
7057
}
7158

7259

73-
@SpringJUnitConfig
74-
static class ZeroCandidatesTestCase {
75-
76-
@MockitoBean
77-
ExampleService example;
78-
79-
@Test
80-
void test() {
81-
assertThat(example).isNotNull();
82-
}
83-
84-
@Configuration
85-
static class Config {
86-
}
87-
}
88-
8960
@SpringJUnitConfig
9061
static class TooManyCandidatesTestCase {
9162

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

+19
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
@SpringJUnitConfig
5050
public class MockitoBeanByTypeIntegrationTests {
5151

52+
@MockitoBean
53+
AnotherService serviceIsNotABean;
54+
5255
@MockitoBean
5356
ExampleService anyNameForService;
5457

@@ -60,6 +63,17 @@ public class MockitoBeanByTypeIntegrationTests {
6063
@CustomQualifier
6164
StringBuilder ambiguousMeta;
6265

66+
@Test
67+
void mockIsCreatedWhenNoCandidateIsFound() {
68+
assertThat(this.serviceIsNotABean)
69+
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue());
70+
71+
when(this.serviceIsNotABean.hello()).thenReturn("Mocked hello");
72+
73+
assertThat(this.serviceIsNotABean.hello()).isEqualTo("Mocked hello");
74+
verify(this.serviceIsNotABean, times(1)).hello();
75+
verifyNoMoreInteractions(this.serviceIsNotABean);
76+
}
6377

6478
@Test
6579
void overrideIsFoundByType(ApplicationContext ctx) {
@@ -109,6 +123,11 @@ void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx
109123
verifyNoMoreInteractions(this.ambiguousMeta);
110124
}
111125

126+
interface AnotherService {
127+
128+
String hello();
129+
130+
}
112131

113132
@Configuration
114133
static class Config {

0 commit comments

Comments
 (0)