Support Bean Overrides for non-singletons
Prior to this commit, the BeanOverrideBeanFactoryPostProcessor rejected any attempt to override a non-singleton bean; however, due to interest from the community, we have decided to provide support for overriding non-singleton beans via the Bean Override mechanism — for example, when using @MockitoBean, @MockitoSpyBean, and @TestBean. With this commit, we now support Bean Overrides for non-singletons: for standard JVM runtimes as well as AOT processing and AOT runtimes. This commit also documents that non-singletons will effectively be converted to singletons when overridden and logs a warning similar to the following. WARN: BeanOverrideBeanFactoryPostProcessor - Converting 'prototype' scoped bean definition 'myBean' to a singleton. See gh-33602 See gh-32933 See gh-33800 Closes gh-35574
This commit is contained in:
parent
ff9a349271
commit
30db2e4fb5
|
@ -88,14 +88,20 @@ To avoid such undesired side effects, consider using
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
When using `@MockitoBean` to mock a non-singleton bean, the non-singleton bean will be
|
||||
replaced with a singleton mock, and the corresponding bean definition will be converted
|
||||
to a `singleton`. Consequently, if you mock a `prototype` or scoped bean, the mock will
|
||||
be treated as a `singleton`.
|
||||
|
||||
Similarly, when using `@MockitoSpyBean` to create a spy for a non-singleton bean, the
|
||||
corresponding bean definition will be converted to a `singleton`. Consequently, if you
|
||||
create a spy for a `prototype` or scoped bean, the spy will be treated as a `singleton`.
|
||||
|
||||
When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean`
|
||||
will be replaced with a singleton mock of the type of object created by the `FactoryBean`.
|
||||
|
||||
When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created
|
||||
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
Similarly, when using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will
|
||||
be created for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
|
|
|
@ -116,12 +116,15 @@ fully-qualified method name following the syntax `<fully-qualified class name>#<
|
|||
– for example, `methodName = "org.example.TestUtils#createCustomService"`.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
[NOTE]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
When overriding a non-singleton bean, the non-singleton bean will be replaced with a
|
||||
singleton bean corresponding to the value returned from the `@TestBean` factory method,
|
||||
and the corresponding bean definition will be converted to a `singleton`. Consequently,
|
||||
if `@TestBean` is used to override a `prototype` or scoped bean, the overridden bean will
|
||||
be treated as a `singleton`.
|
||||
|
||||
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced
|
||||
with a singleton bean corresponding to the value returned from the `@TestBean` factory
|
||||
method.
|
||||
Similarly, when overriding a bean created by a `FactoryBean`, the `FactoryBean` will be
|
||||
replaced with a singleton bean corresponding to the value returned from the `@TestBean`
|
||||
factory method.
|
||||
====
|
||||
|
|
|
@ -62,8 +62,11 @@ defined by the corresponding `BeanOverrideStrategy`:
|
|||
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
When replacing a non-singleton bean, the non-singleton bean will be replaced with a
|
||||
singleton bean corresponding to bean override instance created by the applicable
|
||||
`BeanOverrideHandler`, and the corresponding bean definition will be converted to a
|
||||
`singleton`. Consequently, if a handler overrides a `prototype` or scoped bean, the
|
||||
overridden bean will be treated as a `singleton`.
|
||||
|
||||
When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be
|
||||
replaced with a singleton bean corresponding to bean override instance created by the
|
||||
|
|
|
@ -22,6 +22,8 @@ import java.util.HashSet;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
|
@ -67,6 +69,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
|||
|
||||
private static final String PSEUDO_BEAN_NAME_PLACEHOLDER = "<<< PSEUDO BEAN NAME PLACEHOLDER >>>";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(BeanOverrideBeanFactoryPostProcessor.class);
|
||||
|
||||
private static final BeanNameGenerator beanNameGenerator = DefaultBeanNameGenerator.INSTANCE;
|
||||
|
||||
private final Set<BeanOverrideHandler> beanOverrideHandlers;
|
||||
|
@ -182,10 +186,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
|||
}
|
||||
|
||||
if (existingBeanDefinition != null) {
|
||||
// Validate the existing bean definition.
|
||||
// Process the existing bean definition.
|
||||
//
|
||||
// Applies during "JVM runtime", "AOT processing", and "AOT runtime".
|
||||
validateBeanDefinition(beanFactory, beanName);
|
||||
convertToSingletonIfNecessary(existingBeanDefinition, beanName);
|
||||
}
|
||||
else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
|
||||
// There was no existing bean definition, but during "AOT processing" we
|
||||
|
@ -289,7 +293,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
|||
}
|
||||
}
|
||||
|
||||
validateBeanDefinition(beanFactory, beanName);
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
convertToSingletonIfNecessary(beanDefinition, beanName);
|
||||
this.beanOverrideRegistry.registerBeanOverrideHandler(handler, beanName);
|
||||
}
|
||||
|
||||
|
@ -470,19 +475,20 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
|
||||
* for being replaced by a bean override.
|
||||
* <p>If there is no registered {@code BeanDefinition} for the supplied bean name,
|
||||
* no validation is performed.
|
||||
* Convert the supplied {@link BeanDefinition} for the supplied bean name to
|
||||
* a singleton, if necessary.
|
||||
* @since 7.0
|
||||
*/
|
||||
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
|
||||
private static void convertToSingletonIfNecessary(BeanDefinition beanDefinition, String beanName) {
|
||||
// Due to https://github.com/spring-projects/spring-framework/issues/33800, we do NOT invoke
|
||||
// beanFactory.isSingleton(beanName), since doing so can result in a BeanCreationException for
|
||||
// certain beans -- for example, a Spring Data FactoryBean for a JpaRepository.
|
||||
if (beanFactory.containsBeanDefinition(beanName)) {
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
Assert.state(beanDefinition.isSingleton(),
|
||||
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
|
||||
if (!beanDefinition.isSingleton()) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Converting '%s' scoped bean definition '%s' to a singleton."
|
||||
.formatted(beanDefinition.getScope(), beanName));
|
||||
}
|
||||
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,13 +64,29 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
|
|||
* creation} — for example, based on further processing of the annotation,
|
||||
* the annotated field, or the annotated class.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
|
||||
* Any attempt to override a non-singleton bean will result in an exception.
|
||||
* <h3>Singleton Semantics</h3>
|
||||
*
|
||||
* <p>When replacing a non-singleton bean, the non-singleton bean will be replaced
|
||||
* with a singleton bean corresponding to bean override instance created by the
|
||||
* handler, and the corresponding bean definition will be converted to a singleton.
|
||||
* Consequently, if a handler overrides a prototype or custom scoped bean, the
|
||||
* overridden bean will be treated as a singleton.
|
||||
*
|
||||
* <p>When replacing a bean created by a
|
||||
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the
|
||||
* {@code FactoryBean} itself will be replaced with a singleton bean corresponding
|
||||
* to bean override instance created by the handler.
|
||||
*
|
||||
* <p>When wrapping a bean created by a
|
||||
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the object
|
||||
* created by the {@code FactoryBean} will be wrapped, not the {@code FactoryBean}
|
||||
* itself.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see BeanOverrideStrategy
|
||||
*/
|
||||
public abstract class BeanOverrideHandler {
|
||||
|
||||
|
|
|
@ -109,11 +109,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
|||
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
|
||||
* for further details and examples.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
|
||||
* Any attempt to override a non-singleton bean will result in an exception. When
|
||||
* overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean
|
||||
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean
|
||||
* corresponding to the value returned from the {@code @TestBean} factory method.
|
||||
* <p><strong>NOTE</strong>: When overriding a non-singleton bean, the non-singleton
|
||||
* bean will be replaced with a singleton bean corresponding to the value returned
|
||||
* from the {@code @TestBean} factory method, and the corresponding bean definition
|
||||
* will be converted to a singleton. Consequently, if you override a prototype or
|
||||
* scoped bean, it will be treated as a singleton. Similarly, when overriding a bean
|
||||
* created by a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
|
||||
* the {@code FactoryBean} will be replaced with a singleton bean corresponding to
|
||||
* the value returned from the {@code @TestBean} factory method.
|
||||
*
|
||||
* <p>There are no restrictions on the visibility of {@code @TestBean} fields or
|
||||
* factory methods. Such fields and methods can therefore be {@code public},
|
||||
|
|
|
@ -84,11 +84,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
|||
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
|
||||
* for further details and examples.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be mocked.
|
||||
* Any attempt to mock a non-singleton bean will result in an exception. When
|
||||
* mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean
|
||||
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock
|
||||
* of the type of object created by the {@code FactoryBean}.
|
||||
* <p><strong>NOTE</strong>: When mocking a non-singleton bean, the non-singleton
|
||||
* bean will be replaced with a singleton mock, and the corresponding bean definition
|
||||
* will be converted to a singleton. Consequently, if you mock a prototype or scoped
|
||||
* bean, the mock will be treated as a singleton. Similarly, when mocking a bean
|
||||
* created by a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
|
||||
* the {@code FactoryBean} will be replaced with a singleton mock of the type of
|
||||
* object created by the {@code FactoryBean}.
|
||||
*
|
||||
* <p>There are no restrictions on the visibility of a {@code @MockitoBean} field.
|
||||
* Such fields can therefore be {@code public}, {@code protected}, package-private
|
||||
|
|
|
@ -86,11 +86,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
|||
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
|
||||
* for further details and examples.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied. Any attempt
|
||||
* to create a spy for a non-singleton bean will result in an exception. When
|
||||
* creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
|
||||
* a spy will be created for the object created by the {@code FactoryBean}, not
|
||||
* for the {@code FactoryBean} itself.
|
||||
* <p><strong>NOTE</strong>: When creating a spy for a non-singleton bean, the
|
||||
* corresponding bean definition will be converted to a singleton. Consequently,
|
||||
* if you create a spy for a prototype or scoped bean, the spy will be treated as
|
||||
* a singleton. Similarly, when creating a spy for a
|
||||
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, a spy will
|
||||
* be created for the object created by the {@code FactoryBean}, not for the
|
||||
* {@code FactoryBean} itself.
|
||||
*
|
||||
* <p>There are no restrictions on the visibility of a {@code @MockitoSpyBean} field.
|
||||
* Such fields can therefore be {@code public}, {@code protected}, package-private
|
||||
|
|
|
@ -321,7 +321,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScopeFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScope() {
|
||||
String beanName = "descriptionBean";
|
||||
|
||||
AnnotationConfigApplicationContext context = createContext(ByNameTestCase.class);
|
||||
|
@ -329,13 +329,13 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
|||
definition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
context.registerBeanDefinition(beanName, definition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScopeFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
|
||||
String beanName = "descriptionBean";
|
||||
String scope = "customScope";
|
||||
|
||||
|
@ -346,22 +346,22 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
|||
definition.setScope(scope);
|
||||
context.registerBeanDefinition(beanName, definition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBeanFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBean() {
|
||||
String beanName = "messageServiceBean";
|
||||
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
|
||||
factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
context.registerBeanDefinition(beanName, factoryBeanDefinition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -21,8 +21,10 @@ import org.junit.jupiter.api.Nested;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -43,6 +45,10 @@ public class TestBeanByNameLookupIntegrationTests {
|
|||
@TestBean(name = "methodRenamed1", methodName = "field")
|
||||
String methodRenamed1;
|
||||
|
||||
@TestBean("prototypeScoped")
|
||||
String prototypeScoped;
|
||||
|
||||
|
||||
static String field() {
|
||||
return "fieldOverride";
|
||||
}
|
||||
|
@ -51,6 +57,11 @@ public class TestBeanByNameLookupIntegrationTests {
|
|||
return "nestedFieldOverride";
|
||||
}
|
||||
|
||||
static String prototypeScoped() {
|
||||
return "prototypeScopedOverride";
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
|
@ -63,6 +74,13 @@ public class TestBeanByNameLookupIntegrationTests {
|
|||
assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeScoped").isSingleton()).as("isSingleton").isTrue();
|
||||
assertThat(ctx.getBean("prototypeScoped")).as("applicationContext").isEqualTo("prototypeScopedOverride");
|
||||
assertThat(prototypeScoped).as("injection point").isEqualTo("prototypeScopedOverride");
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @TestBean in enclosing class and in @Nested class")
|
||||
|
@ -180,6 +198,12 @@ public class TestBeanByNameLookupIntegrationTests {
|
|||
String bean4() {
|
||||
return "NestedProd";
|
||||
}
|
||||
|
||||
@Bean("prototypeScoped")
|
||||
@Scope("prototype")
|
||||
String bean5() {
|
||||
return "PrototypeProd";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
|
@ -53,6 +55,9 @@ public class TestBeanByTypeLookupIntegrationTests {
|
|||
@CustomQualifier
|
||||
StringBuilder anyNameForStringBuilder2;
|
||||
|
||||
@TestBean
|
||||
Number prototypeNumber;
|
||||
|
||||
|
||||
static MessageService messageService() {
|
||||
return () -> "mocked nonexistent bean definition";
|
||||
|
@ -70,6 +75,9 @@ public class TestBeanByTypeLookupIntegrationTests {
|
|||
return new StringBuilder("CustomQualifier TestBean String");
|
||||
}
|
||||
|
||||
static Number prototypeNumber() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) {
|
||||
|
@ -101,6 +109,13 @@ public class TestBeanByTypeLookupIntegrationTests {
|
|||
assertThat(ctx.getBean("one")).as("no qualifier needed").hasToString("Prod One");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototypeBeanDefinition(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeNumber").isSingleton()).as("isSingleton").isTrue();
|
||||
assertThat(this.prototypeNumber).isSameAs(ctx.getBean(Number.class));
|
||||
assertThat(this.prototypeNumber).isEqualTo(42);
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
@ -126,6 +141,12 @@ public class TestBeanByTypeLookupIntegrationTests {
|
|||
StringBuilder beanString3() {
|
||||
return new StringBuilder("Prod Three");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
Number prototypeNumber() {
|
||||
return -999;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
|
|
@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
@ -48,6 +50,9 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
|||
@MockitoBean("nonExistingBean")
|
||||
ExampleService nonExisting;
|
||||
|
||||
@MockitoBean("prototypeScoped")
|
||||
ExampleService prototypeScoped;
|
||||
|
||||
|
||||
@Test
|
||||
void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) {
|
||||
|
@ -69,6 +74,17 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
|||
assertThat(nonExisting.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("prototypeScoped"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(MockitoAssertions::assertIsMock)
|
||||
.isSameAs(prototypeScoped);
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeScoped").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(prototypeScoped.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoBean in enclosing class and in @Nested class")
|
||||
|
@ -139,6 +155,12 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
|||
ExampleService bean2() {
|
||||
return new RealExampleService("Hello Nested Field");
|
||||
}
|
||||
|
||||
@Bean("prototypeScoped")
|
||||
@Scope("prototype")
|
||||
ExampleService bean3() {
|
||||
return new RealExampleService("Hello Prototype Field");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test;
|
|||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
|
@ -63,6 +65,10 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
|||
@CustomQualifier
|
||||
StringBuilder ambiguousMeta;
|
||||
|
||||
@MockitoBean
|
||||
YetAnotherService yetAnotherService;
|
||||
|
||||
|
||||
@Test
|
||||
void mockIsCreatedWhenNoCandidateIsFound() {
|
||||
assertIsMock(this.serviceIsNotABean);
|
||||
|
@ -122,11 +128,30 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
|||
verifyNoMoreInteractions(this.ambiguousMeta);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototype(ConfigurableApplicationContext ctx) {
|
||||
assertThat(this.yetAnotherService)
|
||||
.satisfies(MockitoAssertions::assertIsMock)
|
||||
.isSameAs(ctx.getBean("YAS"))
|
||||
.isSameAs(ctx.getBean(YetAnotherService.class));
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("YAS").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
when(this.yetAnotherService.hello()).thenReturn("Mocked greeting");
|
||||
|
||||
assertThat(this.yetAnotherService.hello()).isEqualTo("Mocked greeting");
|
||||
verify(this.yetAnotherService, times(1)).hello();
|
||||
verifyNoMoreInteractions(this.yetAnotherService);
|
||||
}
|
||||
|
||||
|
||||
public interface AnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
public interface YetAnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
@ -150,6 +175,12 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
|||
StringBuilder bean3() {
|
||||
return new StringBuilder("bean3");
|
||||
}
|
||||
|
||||
@Bean("YAS")
|
||||
@Scope("prototype")
|
||||
YetAnotherService bean4() {
|
||||
return () -> "Production Hello";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupIntegrationTests.Config;
|
||||
|
@ -46,6 +48,9 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
|||
@MockitoSpyBean("field1")
|
||||
ExampleService field;
|
||||
|
||||
@MockitoSpyBean("field3")
|
||||
ExampleService prototypeScoped;
|
||||
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
|
@ -57,6 +62,16 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
|||
assertThat(field.greeting()).isEqualTo("bean1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field3"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(MockitoAssertions::assertIsSpy)
|
||||
.isSameAs(prototypeScoped);
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("field3").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(prototypeScoped.greeting()).isEqualTo("bean3");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class")
|
||||
|
@ -102,6 +117,12 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
|||
ExampleService bean2() {
|
||||
return new RealExampleService("bean2");
|
||||
}
|
||||
|
||||
@Bean("field3")
|
||||
@Scope("prototype")
|
||||
ExampleService bean3() {
|
||||
return new RealExampleService("bean3");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
|
@ -55,6 +57,9 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
|||
@CustomQualifier
|
||||
StringHolder ambiguousMeta;
|
||||
|
||||
@MockitoSpyBean
|
||||
AnotherService prototypeService;
|
||||
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByType(ApplicationContext ctx) {
|
||||
|
@ -102,6 +107,19 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
|||
verifyNoMoreInteractions(this.ambiguousMeta);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototype(ConfigurableApplicationContext ctx) {
|
||||
assertThat(this.prototypeService)
|
||||
.satisfies(MockitoAssertions::assertIsSpy)
|
||||
.isSameAs(ctx.getBean("anotherService"))
|
||||
.isSameAs(ctx.getBean(AnotherService.class));
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("anotherService").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(this.prototypeService.hello()).isEqualTo("Production Hello");
|
||||
verify(this.prototypeService).hello();
|
||||
verifyNoMoreInteractions(this.prototypeService);
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
@ -124,6 +142,12 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
|||
StringHolder bean3() {
|
||||
return new StringHolder("bean3");
|
||||
}
|
||||
|
||||
@Bean("anotherService")
|
||||
@Scope("prototype")
|
||||
AnotherService bean4() {
|
||||
return new DefaultAnotherService("Production Hello");
|
||||
}
|
||||
}
|
||||
|
||||
static class StringHolder {
|
||||
|
@ -143,4 +167,17 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
|||
}
|
||||
}
|
||||
|
||||
public interface AnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
record DefaultAnotherService(String message) implements AnotherService {
|
||||
|
||||
@Override
|
||||
public String hello() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue