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
This commit is contained in:
@@ -34,6 +34,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.DependencyDescriptor;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.PriorityOrdered;
|
||||
@@ -63,6 +65,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
|
||||
private final BeanOverrideRegistrar overrideRegistrar;
|
||||
|
||||
private final BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with
|
||||
@@ -133,18 +137,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
String beanNameIncludingFactory;
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (beanName == null) {
|
||||
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
|
||||
int candidateCount = candidateNames.size();
|
||||
if (candidateCount != 1) {
|
||||
Field field = overrideMetadata.getField();
|
||||
throw new IllegalStateException("Unable to select a bean definition to override: found " +
|
||||
candidateCount + " bean definitions of type " + overrideMetadata.getBeanType() +
|
||||
" (as required by annotated field '" + field.getDeclaringClass().getSimpleName() +
|
||||
"." + field.getName() + "')" + (candidateCount > 0 ? ": " + candidateNames : ""));
|
||||
}
|
||||
beanNameIncludingFactory = candidateNames.iterator().next();
|
||||
beanNameIncludingFactory = getBeanNameForType(beanFactory, registry, overrideMetadata, beanDefinition, enforceExistingDefinition);
|
||||
beanName = BeanFactoryUtils.transformedBeanName(beanNameIncludingFactory);
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
if (registry.containsBeanDefinition(beanName)) {
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
|
||||
@@ -176,6 +173,30 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory);
|
||||
}
|
||||
|
||||
private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
|
||||
OverrideMetadata overrideMetadata, RootBeanDefinition beanDefinition, boolean enforceExistingDefinition) {
|
||||
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
|
||||
int candidateCount = candidateNames.size();
|
||||
if (candidateCount == 1) {
|
||||
return candidateNames.iterator().next();
|
||||
}
|
||||
else if (candidateCount == 0) {
|
||||
if (enforceExistingDefinition) {
|
||||
Field field = overrideMetadata.getField();
|
||||
throw new IllegalStateException(
|
||||
"Unable to override bean: no bean definitions of type %s (as required by annotated field '%s.%s')"
|
||||
.formatted(overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
}
|
||||
return this.beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
}
|
||||
Field field = overrideMetadata.getField();
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unable to select a bean definition to override: found %s bean definitions of type %s " +
|
||||
"(as required by annotated field '%s.%s'): %s",
|
||||
candidateCount, overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the expected bean name is registered and matches the type to override.
|
||||
* <p>If so, put the override metadata in the early tracking map.
|
||||
@@ -209,7 +230,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition();
|
||||
RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve());
|
||||
definition.setTargetType(metadata.getBeanType());
|
||||
definition.setQualifiedElement(metadata.getField());
|
||||
return definition;
|
||||
|
||||
@@ -51,8 +51,8 @@ class FailingTestBeanByTypeIntegrationTests {
|
||||
cause(
|
||||
instanceOf(IllegalStateException.class),
|
||||
message("""
|
||||
Unable to select a bean definition to override: found 0 bean definitions \
|
||||
of type %s (as required by annotated field '%s.example')"""
|
||||
Unable to override bean: no bean definitions of type \
|
||||
%s (as required by annotated field '%s.example')"""
|
||||
.formatted(ExampleService.class.getName(), testClass.getSimpleName())))));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,19 +43,6 @@ import static org.junit.platform.testkit.engine.TestExecutionResultConditions.me
|
||||
*/
|
||||
class FailingMockitoBeanByTypeIntegrationTests {
|
||||
|
||||
@Test
|
||||
void zeroCandidates() {
|
||||
Class<?> testClass = ZeroCandidatesTestCase.class;
|
||||
EngineTestKitUtils.executeTestsForClass(testClass).assertThatEvents().haveExactly(1,
|
||||
finishedWithFailure(
|
||||
cause(
|
||||
instanceOf(IllegalStateException.class),
|
||||
message("""
|
||||
Unable to select a bean definition to override: found 0 bean definitions \
|
||||
of type %s (as required by annotated field '%s.example')"""
|
||||
.formatted(ExampleService.class.getName(), testClass.getSimpleName())))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tooManyCandidates() {
|
||||
Class<?> testClass = TooManyCandidatesTestCase.class;
|
||||
@@ -70,22 +57,6 @@ class FailingMockitoBeanByTypeIntegrationTests {
|
||||
}
|
||||
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class ZeroCandidatesTestCase {
|
||||
|
||||
@MockitoBean
|
||||
ExampleService example;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
assertThat(example).isNotNull();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
}
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
static class TooManyCandidatesTestCase {
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@SpringJUnitConfig
|
||||
public class MockitoBeanByTypeIntegrationTests {
|
||||
|
||||
@MockitoBean
|
||||
AnotherService serviceIsNotABean;
|
||||
|
||||
@MockitoBean
|
||||
ExampleService anyNameForService;
|
||||
|
||||
@@ -60,6 +63,17 @@ public class MockitoBeanByTypeIntegrationTests {
|
||||
@CustomQualifier
|
||||
StringBuilder ambiguousMeta;
|
||||
|
||||
@Test
|
||||
void mockIsCreatedWhenNoCandidateIsFound() {
|
||||
assertThat(this.serviceIsNotABean)
|
||||
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue());
|
||||
|
||||
when(this.serviceIsNotABean.hello()).thenReturn("Mocked hello");
|
||||
|
||||
assertThat(this.serviceIsNotABean.hello()).isEqualTo("Mocked hello");
|
||||
verify(this.serviceIsNotABean, times(1)).hello();
|
||||
verifyNoMoreInteractions(this.serviceIsNotABean);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByType(ApplicationContext ctx) {
|
||||
@@ -109,6 +123,11 @@ public class MockitoBeanByTypeIntegrationTests {
|
||||
verifyNoMoreInteractions(this.ambiguousMeta);
|
||||
}
|
||||
|
||||
interface AnotherService {
|
||||
|
||||
String hello();
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
Reference in New Issue
Block a user