Browse Source

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
pull/35588/head
Sam Brannen 2 months ago
parent
commit
30db2e4fb5
  1. 14
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
  2. 17
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc
  3. 7
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc
  4. 30
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
  5. 20
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
  6. 13
      spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
  7. 12
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
  8. 12
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
  9. 24
      spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java
  10. 24
      spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByNameLookupIntegrationTests.java
  11. 21
      spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeLookupIntegrationTests.java
  12. 22
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByNameLookupIntegrationTests.java
  13. 31
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupIntegrationTests.java
  14. 21
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanByNameLookupIntegrationTests.java
  15. 37
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanByTypeLookupIntegrationTests.java

14
framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc

@ -88,14 +88,20 @@ To avoid such undesired side effects, consider using
[NOTE] [NOTE]
==== ====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean When using `@MockitoBean` to mock a non-singleton bean, the non-singleton bean will be
will result in an exception. 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` 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`. 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 Similarly, when using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will
for the object created by the `FactoryBean`, not for the `FactoryBean` itself. be created for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
==== ====
[NOTE] [NOTE]

17
framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc

@ -116,12 +116,15 @@ fully-qualified method name following the syntax `<fully-qualified class name>#<
– for example, `methodName = "org.example.TestUtils#createCustomService"`. – for example, `methodName = "org.example.TestUtils#createCustomService"`.
==== ====
[TIP] [NOTE]
==== ====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean When overriding a non-singleton bean, the non-singleton bean will be replaced with a
will result in an exception. singleton bean corresponding to the value returned from the `@TestBean` factory method,
and the corresponding bean definition will be converted to a `singleton`. Consequently,
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced if `@TestBean` is used to override a `prototype` or scoped bean, the overridden bean will
with a singleton bean corresponding to the value returned from the `@TestBean` factory be treated as a `singleton`.
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.
==== ====

7
framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc

@ -62,8 +62,11 @@ defined by the corresponding `BeanOverrideStrategy`:
[TIP] [TIP]
==== ====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean When replacing a non-singleton bean, the non-singleton bean will be replaced with a
will result in an exception. 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 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 replaced with a singleton bean corresponding to bean override instance created by the

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

@ -22,6 +22,8 @@ import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aop.scope.ScopedProxyUtils; 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 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 static final BeanNameGenerator beanNameGenerator = DefaultBeanNameGenerator.INSTANCE;
private final Set<BeanOverrideHandler> beanOverrideHandlers; private final Set<BeanOverrideHandler> beanOverrideHandlers;
@ -182,10 +186,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
} }
if (existingBeanDefinition != null) { if (existingBeanDefinition != null) {
// Validate the existing bean definition. // Process the existing bean definition.
// //
// Applies during "JVM runtime", "AOT processing", and "AOT runtime". // Applies during "JVM runtime", "AOT processing", and "AOT runtime".
validateBeanDefinition(beanFactory, beanName); convertToSingletonIfNecessary(existingBeanDefinition, beanName);
} }
else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) { else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
// There was no existing bean definition, but during "AOT processing" we // 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); 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 * Convert the supplied {@link BeanDefinition} for the supplied bean name to
* for being replaced by a bean override. * a singleton, if necessary.
* <p>If there is no registered {@code BeanDefinition} for the supplied bean name, * @since 7.0
* no validation is performed.
*/ */
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 // 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 // beanFactory.isSingleton(beanName), since doing so can result in a BeanCreationException for
// certain beans -- for example, a Spring Data FactoryBean for a JpaRepository. // certain beans -- for example, a Spring Data FactoryBean for a JpaRepository.
if (beanFactory.containsBeanDefinition(beanName)) { if (!beanDefinition.isSingleton()) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); if (logger.isWarnEnabled()) {
Assert.state(beanDefinition.isSingleton(), logger.warn("Converting '%s' scoped bean definition '%s' to a singleton."
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden."); .formatted(beanDefinition.getScope(), beanName));
}
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
} }
} }

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

@ -64,13 +64,29 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
* creation} &mdash; for example, based on further processing of the annotation, * creation} &mdash; for example, based on further processing of the annotation,
* the annotated field, or the annotated class. * the annotated field, or the annotated class.
* *
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden. * <h3>Singleton Semantics</h3>
* Any attempt to override a non-singleton bean will result in an exception. *
* <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 Simon Baslé
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Sam Brannen * @author Sam Brannen
* @since 6.2 * @since 6.2
* @see BeanOverrideStrategy
*/ */
public abstract class BeanOverrideHandler { public abstract class BeanOverrideHandler {

13
spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java

@ -109,11 +109,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples. * for further details and examples.
* *
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden. * <p><strong>NOTE</strong>: When overriding a non-singleton bean, the non-singleton
* Any attempt to override a non-singleton bean will result in an exception. When * bean will be replaced with a singleton bean corresponding to the value returned
* overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean * from the {@code @TestBean} factory method, and the corresponding bean definition
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean * will be converted to a singleton. Consequently, if you override a prototype or
* corresponding to the value returned from the {@code @TestBean} factory method. * 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 * <p>There are no restrictions on the visibility of {@code @TestBean} fields or
* factory methods. Such fields and methods can therefore be {@code public}, * factory methods. Such fields and methods can therefore be {@code public},

12
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java

@ -84,11 +84,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples. * for further details and examples.
* *
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be mocked. * <p><strong>NOTE</strong>: When mocking a non-singleton bean, the non-singleton
* Any attempt to mock a non-singleton bean will result in an exception. When * bean will be replaced with a singleton mock, and the corresponding bean definition
* mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean * will be converted to a singleton. Consequently, if you mock a prototype or scoped
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock * bean, the mock will be treated as a singleton. Similarly, when mocking a bean
* of the type of object created by the {@code FactoryBean}. * 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. * <p>There are no restrictions on the visibility of a {@code @MockitoBean} field.
* Such fields can therefore be {@code public}, {@code protected}, package-private * Such fields can therefore be {@code public}, {@code protected}, package-private

12
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java

@ -86,11 +86,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples. * for further details and examples.
* *
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied. Any attempt * <p><strong>NOTE</strong>: When creating a spy for a non-singleton bean, the
* to create a spy for a non-singleton bean will result in an exception. When * corresponding bean definition will be converted to a singleton. Consequently,
* creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean}, * if you create a spy for a prototype or scoped bean, the spy will be treated as
* a spy will be created for the object created by the {@code FactoryBean}, not * a singleton. Similarly, when creating a spy for a
* for the {@code FactoryBean} itself. * {@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. * <p>There are no restrictions on the visibility of a {@code @MockitoSpyBean} field.
* Such fields can therefore be {@code public}, {@code protected}, package-private * Such fields can therefore be {@code public}, {@code protected}, package-private

24
spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java

@ -321,7 +321,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
} }
@Test @Test
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScopeFails() { void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScope() {
String beanName = "descriptionBean"; String beanName = "descriptionBean";
AnnotationConfigApplicationContext context = createContext(ByNameTestCase.class); AnnotationConfigApplicationContext context = createContext(ByNameTestCase.class);
@ -329,13 +329,13 @@ class BeanOverrideBeanFactoryPostProcessorTests {
definition.setScope(BeanDefinition.SCOPE_PROTOTYPE); definition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
context.registerBeanDefinition(beanName, definition); context.registerBeanDefinition(beanName, definition);
assertThatIllegalStateException() assertThatNoException().isThrownBy(context::refresh);
.isThrownBy(context::refresh) assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden."); assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
} }
@Test @Test
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScopeFails() { void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
String beanName = "descriptionBean"; String beanName = "descriptionBean";
String scope = "customScope"; String scope = "customScope";
@ -346,22 +346,22 @@ class BeanOverrideBeanFactoryPostProcessorTests {
definition.setScope(scope); definition.setScope(scope);
context.registerBeanDefinition(beanName, definition); context.registerBeanDefinition(beanName, definition);
assertThatIllegalStateException() assertThatNoException().isThrownBy(context::refresh);
.isThrownBy(context::refresh) assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden."); assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
} }
@Test @Test
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBeanFails() { void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBean() {
String beanName = "messageServiceBean"; String beanName = "messageServiceBean";
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class); AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class); RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
context.registerBeanDefinition(beanName, factoryBeanDefinition); context.registerBeanDefinition(beanName, factoryBeanDefinition);
assertThatIllegalStateException() assertThatNoException().isThrownBy(context::refresh);
.isThrownBy(context::refresh) assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden."); assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
} }
@Test @Test

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

@ -21,8 +21,10 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -43,6 +45,10 @@ public class TestBeanByNameLookupIntegrationTests {
@TestBean(name = "methodRenamed1", methodName = "field") @TestBean(name = "methodRenamed1", methodName = "field")
String methodRenamed1; String methodRenamed1;
@TestBean("prototypeScoped")
String prototypeScoped;
static String field() { static String field() {
return "fieldOverride"; return "fieldOverride";
} }
@ -51,6 +57,11 @@ public class TestBeanByNameLookupIntegrationTests {
return "nestedFieldOverride"; return "nestedFieldOverride";
} }
static String prototypeScoped() {
return "prototypeScopedOverride";
}
@Test @Test
void fieldHasOverride(ApplicationContext ctx) { void fieldHasOverride(ApplicationContext ctx) {
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
@ -63,6 +74,13 @@ public class TestBeanByNameLookupIntegrationTests {
assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride"); 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 @Nested
@DisplayName("With @TestBean in enclosing class and in @Nested class") @DisplayName("With @TestBean in enclosing class and in @Nested class")
@ -180,6 +198,12 @@ public class TestBeanByNameLookupIntegrationTests {
String bean4() { String bean4() {
return "NestedProd"; return "NestedProd";
} }
@Bean("prototypeScoped")
@Scope("prototype")
String bean5() {
return "PrototypeProd";
}
} }
} }

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

@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.CustomQualifier;
import org.springframework.test.context.bean.override.example.ExampleService; 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.example.RealExampleService;
@ -53,6 +55,9 @@ public class TestBeanByTypeLookupIntegrationTests {
@CustomQualifier @CustomQualifier
StringBuilder anyNameForStringBuilder2; StringBuilder anyNameForStringBuilder2;
@TestBean
Number prototypeNumber;
static MessageService messageService() { static MessageService messageService() {
return () -> "mocked nonexistent bean definition"; return () -> "mocked nonexistent bean definition";
@ -70,6 +75,9 @@ public class TestBeanByTypeLookupIntegrationTests {
return new StringBuilder("CustomQualifier TestBean String"); return new StringBuilder("CustomQualifier TestBean String");
} }
static Number prototypeNumber() {
return 42;
}
@Test @Test
void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) { void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) {
@ -101,6 +109,13 @@ public class TestBeanByTypeLookupIntegrationTests {
assertThat(ctx.getBean("one")).as("no qualifier needed").hasToString("Prod One"); 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) @Configuration(proxyBeanMethods = false)
static class Config { static class Config {
@ -126,6 +141,12 @@ public class TestBeanByTypeLookupIntegrationTests {
StringBuilder beanString3() { StringBuilder beanString3() {
return new StringBuilder("Prod Three"); return new StringBuilder("Prod Three");
} }
@Bean
@Scope("prototype")
Number prototypeNumber() {
return -999;
}
} }
@FunctionalInterface @FunctionalInterface

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

@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.ExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.bean.override.example.RealExampleService;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@ -48,6 +50,9 @@ public class MockitoBeanByNameLookupIntegrationTests {
@MockitoBean("nonExistingBean") @MockitoBean("nonExistingBean")
ExampleService nonExisting; ExampleService nonExisting;
@MockitoBean("prototypeScoped")
ExampleService prototypeScoped;
@Test @Test
void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) {
@ -69,6 +74,17 @@ public class MockitoBeanByNameLookupIntegrationTests {
assertThat(nonExisting.greeting()).as("mocked greeting").isNull(); 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 @Nested
@DisplayName("With @MockitoBean in enclosing class and in @Nested class") @DisplayName("With @MockitoBean in enclosing class and in @Nested class")
@ -139,6 +155,12 @@ public class MockitoBeanByNameLookupIntegrationTests {
ExampleService bean2() { ExampleService bean2() {
return new RealExampleService("Hello Nested Field"); return new RealExampleService("Hello Nested Field");
} }
@Bean("prototypeScoped")
@Scope("prototype")
ExampleService bean3() {
return new RealExampleService("Hello Prototype Field");
}
} }
} }

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

@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.test.context.bean.override.example.CustomQualifier; 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.ExampleService;
@ -63,6 +65,10 @@ public class MockitoBeanByTypeLookupIntegrationTests {
@CustomQualifier @CustomQualifier
StringBuilder ambiguousMeta; StringBuilder ambiguousMeta;
@MockitoBean
YetAnotherService yetAnotherService;
@Test @Test
void mockIsCreatedWhenNoCandidateIsFound() { void mockIsCreatedWhenNoCandidateIsFound() {
assertIsMock(this.serviceIsNotABean); assertIsMock(this.serviceIsNotABean);
@ -122,11 +128,30 @@ public class MockitoBeanByTypeLookupIntegrationTests {
verifyNoMoreInteractions(this.ambiguousMeta); 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 { public interface AnotherService {
String hello(); String hello();
}
public interface YetAnotherService {
String hello();
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ -150,6 +175,12 @@ public class MockitoBeanByTypeLookupIntegrationTests {
StringBuilder bean3() { StringBuilder bean3() {
return new StringBuilder("bean3"); return new StringBuilder("bean3");
} }
@Bean("YAS")
@Scope("prototype")
YetAnotherService bean4() {
return () -> "Production Hello";
}
} }
} }

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

@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.ExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.bean.override.example.RealExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupIntegrationTests.Config; import org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupIntegrationTests.Config;
@ -46,6 +48,9 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
@MockitoSpyBean("field1") @MockitoSpyBean("field1")
ExampleService field; ExampleService field;
@MockitoSpyBean("field3")
ExampleService prototypeScoped;
@Test @Test
void fieldHasOverride(ApplicationContext ctx) { void fieldHasOverride(ApplicationContext ctx) {
@ -57,6 +62,16 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
assertThat(field.greeting()).isEqualTo("bean1"); 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 @Nested
@DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class") @DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class")
@ -102,6 +117,12 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
ExampleService bean2() { ExampleService bean2() {
return new RealExampleService("bean2"); return new RealExampleService("bean2");
} }
@Bean("field3")
@Scope("prototype")
ExampleService bean3() {
return new RealExampleService("bean3");
}
} }
} }

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

@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.test.context.bean.override.example.CustomQualifier; 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.ExampleService;
@ -55,6 +57,9 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
@CustomQualifier @CustomQualifier
StringHolder ambiguousMeta; StringHolder ambiguousMeta;
@MockitoSpyBean
AnotherService prototypeService;
@Test @Test
void overrideIsFoundByType(ApplicationContext ctx) { void overrideIsFoundByType(ApplicationContext ctx) {
@ -102,6 +107,19 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
verifyNoMoreInteractions(this.ambiguousMeta); 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) @Configuration(proxyBeanMethods = false)
static class Config { static class Config {
@ -124,6 +142,12 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
StringHolder bean3() { StringHolder bean3() {
return new StringHolder("bean3"); return new StringHolder("bean3");
} }
@Bean("anotherService")
@Scope("prototype")
AnotherService bean4() {
return new DefaultAnotherService("Production Hello");
}
} }
static class StringHolder { 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…
Cancel
Save