Browse Source

Bean overriding by type uses isAutowireCandidate for matching

This commit uses the bean factory `isAutowiredCandidate` method directly
in `BeanOverrideBeanFactoryPostProcessor` to select a single match among
multiple candidates when matching by type.

The expected consequence, in most cases, is that this will delegate to
a `@Qualifier`-aware `QualifierAnnotationAutowireCandidateResolver`.
In that sense, bean overriding by-type matching is now potentially
taking Qualifier annotations or meta-annotations into account.

It also changes the way existing bean definitions are checked in case
a bean name has been specified: factory beans are now taken into account
when checking the type of an existing definition matches the expected
bean override type.

Closes gh-32822
pull/32874/head
Simon Baslé 2 years ago
parent
commit
d5c7a5e2db
  1. 13
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
  2. 8
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc
  3. 8
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc
  4. 63
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
  5. 5
      spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
  6. 3
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
  7. 3
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
  8. 38
      spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java
  9. 50
      spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java
  10. 39
      spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java
  11. 32
      spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java
  12. 100
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoByTypeIntegrationTests.java

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

@ -6,17 +6,18 @@ the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the @@ -6,17 +6,18 @@ the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the
case, the original bean definition is not replaced, but instead an early instance of the
bean is captured and wrapped by the spy.
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
typically by specifying a bean `name` in the annotation.
If no bean `name` is specified, the annotated field's type is used to search for candidate
definitions to override.
By default, the annotated field's type is used to search for candidate definitions to
override, but note that `@Qualifier` annotations are also taken into account for the
purpose of matching. Users can also make things entirely explicit by specifying a bean
`name` in the annotation.
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
It requires that at most one candidate definition exists if a bean name is specified,
or exactly one if no bean name is specified.
It requires that at most one matching candidate definition exists if a bean name
is specified, or exactly one if no bean name is specified.
The `@MockitoSpyBean` annotation uses the `WRAP_BEAN`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],

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

@ -11,10 +11,10 @@ but the annotation allows for a specific method name to be provided. @@ -11,10 +11,10 @@ but the annotation allows for a specific method name to be provided.
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
typically by specifying a bean `name` in the annotation.
If no bean `name` is specified, the annotated field's type is used to search for candidate
definitions to override. In that case it is required that exactly one definition matches.
By default, the annotated field's type is used to search for candidate definitions to override.
In that case it is required that exactly one definition matches, but note that `@Qualifier`
annotations are also taken into account for the purpose of matching.
Users can also make things entirely explicit by specifying a bean `name` in the annotation.
The following example shows how to fully configure the `@TestBean` annotation, with
explicit values equivalent to the defaults:

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

@ -61,11 +61,11 @@ In contrast to Spring's autowiring mechanism (for example, resolution of an `@Au @@ -61,11 +61,11 @@ In contrast to Spring's autowiring mechanism (for example, resolution of an `@Au
field), the bean overriding infrastructure in the TestContext framework has limited
heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` can compute
the name of the bean to override, or it can be unambiguously selected given the type of
the annotated field.
the annotated field and its qualifying annotations.
Typically, the bean is selected by type by the `BeanOverrideFactoryPostProcessor`.
Alternatively, the user can directly provide the bean name in the custom annotation.
Typically, the user directly provides the bean name in the custom annotation in order to
make things as explicit as possible. Alternatively, the bean is selected by type by the
`BeanOverrideFactoryPostProcessor`.
Some `BeanOverrideProcessor`s could also internally compute a bean name based on a
convention or another advanced method.
====

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

@ -31,6 +31,7 @@ import org.springframework.beans.factory.FactoryBean; @@ -31,6 +31,7 @@ import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
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.RootBeanDefinition;
@ -127,28 +128,34 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -127,28 +128,34 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
String beanName = overrideMetadata.getBeanName();
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType());
if (candidates.length != 1) {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
if (candidates.size() != 1) {
Field f = overrideMetadata.getField();
throw new IllegalStateException("Unable to select a bean definition to override, " +
candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() +
candidates.size() + " bean definitions found of type " + overrideMetadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = candidates[0];
beanName = candidates.iterator().next();
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
if (candidates.contains(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else if (enforceExistingDefinition) {
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
" bean definition to replace with that name of type " + overrideMetadata.getBeanType());
}
}
BeanDefinition existingBeanDefinition = null;
if (beanFactory.containsBeanDefinition(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
if (existingBeanDefinition != null) {
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
registry.removeBeanDefinition(beanName);
}
else if (enforceExistingDefinition) {
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
" bean definition to replace with that name");
}
registry.registerBeanDefinition(beanName, beanDefinition);
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
@ -171,33 +178,40 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -171,33 +178,40 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* phase.
*/
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
String beanName = metadata.getBeanName();
if (beanName == null) {
if (existingBeanNames.size() != 1) {
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, metadata, true);
if (candidateNames.size() != 1) {
Field f = metadata.getField();
throw new IllegalStateException("Unable to select a bean to override by wrapping, " +
existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() +
candidateNames.size() + " bean instances found of type " + metadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = existingBeanNames.iterator().next();
beanName = candidateNames.iterator().next();
}
else if (!existingBeanNames.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " +
"there is no existing bean instance with that name of type " + metadata.getBeanType());
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, metadata, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; there is no" +
" existing bean instance with that name of type " + metadata.getBeanType());
}
}
this.overrideRegistrar.markWrapEarly(metadata, beanName);
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
}
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition definition = new RootBeanDefinition();
definition.setTargetType(metadata.getBeanType());
definition.setQualifiedElement(metadata.getField());
return definition;
}
private Set<String> getExistingBeanNames(ConfigurableListableBeanFactory beanFactory, ResolvableType resolvableType) {
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata,
boolean checkAutowiredCandidate) {
ResolvableType resolvableType = metadata.getBeanType();
Set<String> beans = new LinkedHashSet<>(
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
Class<?> type = resolvableType.resolve(Object.class);
@ -209,7 +223,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -209,7 +223,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
beans.add(beanName);
}
}
beans.removeIf(ScopedProxyUtils::isScopedTarget);
if (checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(metadata.getField(), true);
beans.removeIf(beanName -> ScopedProxyUtils.isScopedTarget(beanName) ||
!beanFactory.isAutowireCandidate(beanName, descriptor));
}
else {
beans.removeIf(ScopedProxyUtils::isScopedTarget);
}
return beans;
}

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

@ -30,8 +30,9 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -30,8 +30,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>By default, the bean to override is inferred from the type of the
* annotated field. This requires that exactly one matching definition is
* present in the application context. To explicitly specify a bean name to
* replace, set the {@link #value()} or {@link #name()} attribute.
* present in the application context. A {@code @Qualifier} annotation can be
* used to help disambiguate. Alternatively, you can explicitly specify a bean
* name to replace by setting the {@link #value()} or {@link #name()} attribute.
*
* <p>The instance is created from a zero-argument static factory method in the
* test class whose return type is compatible with the annotated field. In the

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

@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>If no explicit {@link #name()} is specified, a target bean definition is
* selected according to the class of the annotated field, and there must be
* exactly one such candidate definition in the context.
* exactly one such candidate definition in the context. A {@code @Qualifier}
* annotation can be used to help disambiguate.
* If a {@link #name()} is specified, either the definition exists in the
* application context and is replaced, or it doesn't and a new one is added to
* the context.

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

@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>If no explicit {@link #name()} is specified, a target bean is selected
* according to the class of the annotated field, and there must be exactly one
* such candidate bean.
* such candidate bean. A {@code @Qualifier} annotation can be used to help
* disambiguate.
* If a {@link #name()} is specified, it is required that a target bean of that
* name has been previously registered in the application context.
*

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

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@ -75,8 +77,8 @@ class BeanOverrideBeanFactoryPostProcessorTests { @@ -75,8 +77,8 @@ class BeanOverrideBeanFactoryPostProcessorTests {
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("Unable to override bean 'explicit'; " +
"there is no bean definition to replace with that name");
.withMessage("Unable to override bean 'explicit'; there is no bean definition " +
"to replace with that name of type org.springframework.test.context.bean.override.example.ExampleService");
}
@Test
@ -176,6 +178,26 @@ class BeanOverrideBeanFactoryPostProcessorTests { @@ -176,6 +178,26 @@ class BeanOverrideBeanFactoryPostProcessorTests {
.matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype");
}
@Test
void createDefinitionShouldSetQualifierElement() {
AnnotationConfigApplicationContext context = createContext(QualifiedBean.class);
context.registerBeanDefinition("singleton", new RootBeanDefinition(String.class, () -> "ORIGINAL"));
context.register(QualifiedBean.class);
assertThatNoException().isThrownBy(context::refresh);
assertThat(context.getBeanDefinition("singleton"))
.isInstanceOfSatisfying(RootBeanDefinition.class, this::isTheValueField);
}
private void isTheValueField(RootBeanDefinition def) {
assertThat(def.getQualifiedElement()).isInstanceOfSatisfying(Field.class, field -> {
assertThat(field.getDeclaringClass()).isEqualTo(QualifiedBean.class);
assertThat(field.getName()).as("annotated field name")
.isEqualTo("value");
});
}
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@ -260,6 +282,18 @@ class BeanOverrideBeanFactoryPostProcessorTests { @@ -260,6 +282,18 @@ class BeanOverrideBeanFactoryPostProcessorTests {
}
}
static class QualifiedBean {
@Qualifier("preferThis")
@ExampleBeanOverrideAnnotation(beanName = "singleton",
value = "useThis", createIfMissing = false)
private String value;
static String useThis() {
return "USED THIS";
}
}
static class TestFactoryBean implements FactoryBean<Object> {
@Override

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

@ -21,9 +21,11 @@ import org.junit.platform.engine.TestExecutionResult; @@ -21,9 +21,11 @@ import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@ -39,10 +41,26 @@ public class TestBeanByTypeIntegrationTests { @@ -39,10 +41,26 @@ public class TestBeanByTypeIntegrationTests {
@TestBean
ExampleService anyNameForService;
@TestBean(methodName = "someString")
@Qualifier("prefer")
StringBuilder anyNameForStringBuilder;
@TestBean(methodName = "someString2")
@CustomQualifier
StringBuilder anyNameForStringBuilder2;
static ExampleService anyNameForServiceTestOverride() {
return new RealExampleService("Mocked greeting");
}
static StringBuilder someString() {
return new StringBuilder("Prefer TestBean String");
}
static StringBuilder someString2() {
return new StringBuilder("CustomQualifier TestBean String");
}
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
@ -52,6 +70,21 @@ public class TestBeanByTypeIntegrationTests { @@ -52,6 +70,21 @@ public class TestBeanByTypeIntegrationTests {
assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting");
}
@Test
void overrideIsFoundByTypeWithQualifierDisambiguation(ApplicationContext ctx) {
assertThat(this.anyNameForStringBuilder)
.as("direct qualifier")
.isSameAs(ctx.getBean("two"))
.hasToString("Prefer TestBean String");
assertThat(this.anyNameForStringBuilder2)
.as("meta qualifier")
.isSameAs(ctx.getBean("three"))
.hasToString("CustomQualifier TestBean String");
assertThat(ctx.getBean("one")).as("no qualifier needed").hasToString("Prod One");
}
@Test
void zeroCandidates() {
Class<?> caseClass = CaseNone.class;
@ -92,6 +125,23 @@ public class TestBeanByTypeIntegrationTests { @@ -92,6 +125,23 @@ public class TestBeanByTypeIntegrationTests {
ExampleService bean1() {
return new RealExampleService("Production hello");
}
@Bean("one")
StringBuilder beanString1() {
return new StringBuilder("Prod One");
}
@Bean("two")
@Qualifier("prefer")
StringBuilder beanString2() {
return new StringBuilder("Prod Two");
}
@Bean("three")
@CustomQualifier
StringBuilder beanString3() {
return new StringBuilder("Prod Three");
}
}
@SpringJUnitConfig(FailingNone.class)

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

@ -92,7 +92,7 @@ public class TestBeanIntegrationTests { @@ -92,7 +92,7 @@ public class TestBeanIntegrationTests {
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'noOriginalBean'; " +
"there is no bean definition to replace with that name"));
"there is no bean definition to replace with that name of type java.lang.String"));
}
@Test
@ -107,7 +107,7 @@ public class TestBeanIntegrationTests { @@ -107,7 +107,7 @@ public class TestBeanIntegrationTests {
.cause()
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'notPresent'; " +
"there is no bean definition to replace with that name"));
"there is no bean definition to replace with that name of type java.lang.String"));
}
@Test
@ -142,6 +142,20 @@ public class TestBeanIntegrationTests { @@ -142,6 +142,20 @@ public class TestBeanIntegrationTests {
"supported candidates [fieldTestOverride]"));
}
@Test
void testBeanFailingBeanOfWrongType() {
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
.selectors(selectClass(Failing5.class))//
.execute();
assertThat(results.allEvents().failed().stream()).hasSize(1).first()
.satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
.getThrowable()).get(THROWABLE)
.rootCause().isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to override bean 'notString'; there is no bean definition to replace with " +
"that name of type java.lang.String"));
}
@Nested
@DisplayName("With @TestBean on enclosing class")
class TestBeanNested {
@ -262,4 +276,25 @@ public class TestBeanIntegrationTests { @@ -262,4 +276,25 @@ public class TestBeanIntegrationTests {
fail("should fail earlier");
}
}
@SpringJUnitConfig
static class Failing5 {
@Bean("notString")
StringBuilder bean1() {
return new StringBuilder("not a String");
}
@TestBean(name = "notString")
String field;
@Test
void ignored() {
fail("should fail earlier");
}
static String fieldTestOverride() {
return "should be ignored";
}
}
}

32
spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
/*
* 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.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Qualifier
public @interface CustomQualifier {
}

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

@ -23,16 +23,21 @@ import org.junit.platform.testkit.engine.EngineExecutionResults; @@ -23,16 +23,21 @@ import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -47,6 +52,14 @@ public class MockitoByTypeIntegrationTests { @@ -47,6 +52,14 @@ public class MockitoByTypeIntegrationTests {
@MockitoBean
ExampleService anyNameForService;
@MockitoBean
@Qualifier("prefer")
StringBuilder ambiguous;
@MockitoBean
@CustomQualifier
StringBuilder ambiguousMeta;
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
@ -62,6 +75,39 @@ public class MockitoByTypeIntegrationTests { @@ -62,6 +75,39 @@ public class MockitoByTypeIntegrationTests {
verifyNoMoreInteractions(this.anyNameForService);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
assertThat(this.ambiguous)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(ctx.getBean("ambiguous2"));
assertThatException().isThrownBy(() -> ctx.getBean(StringBuilder.class))
.withMessageEndingWith("but found 2: ambiguous2,ambiguous1");
assertThat(this.ambiguous.length()).isZero();
assertThat(this.ambiguous.substring(0)).isNull();
verify(this.ambiguous, times(1)).length();
verify(this.ambiguous, times(1)).substring(anyInt());
verifyNoMoreInteractions(this.ambiguous);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
assertThat(this.ambiguousMeta)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
.as("isMock").isTrue())
.isSameAs(ctx.getBean("ambiguous1"));
assertThatException().isThrownBy(() -> ctx.getBean(StringBuilder.class))
.withMessageEndingWith("but found 2: ambiguous2,ambiguous1");
assertThat(this.ambiguousMeta.length()).isZero();
assertThat(this.ambiguousMeta.substring(0)).isNull();
verify(this.ambiguousMeta, times(1)).length();
verify(this.ambiguousMeta, times(1)).substring(anyInt());
verifyNoMoreInteractions(this.ambiguousMeta);
}
@Test
void zeroCandidates() {
@ -127,6 +173,14 @@ public class MockitoByTypeIntegrationTests { @@ -127,6 +173,14 @@ public class MockitoByTypeIntegrationTests {
@MockitoSpyBean
ExampleService anyNameForService;
@MockitoSpyBean
@Qualifier("prefer")
StringBuilder ambiguous;
@MockitoSpyBean
@CustomQualifier
StringBuilder ambiguousMeta;
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
@ -140,6 +194,38 @@ public class MockitoByTypeIntegrationTests { @@ -140,6 +194,38 @@ public class MockitoByTypeIntegrationTests {
verifyNoMoreInteractions(this.anyNameForService);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
assertThat(this.ambiguous)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(ctx.getBean("ambiguous2"));
assertThatException().isThrownBy(() -> ctx.getBean(StringBuilder.class))
.withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
assertThat(this.ambiguous.toString()).isEqualTo("bean3");
assertThat(this.ambiguous.length()).isEqualTo(5);
verify(this.ambiguous, times(1)).length();
verifyNoMoreInteractions(this.ambiguous); //mockito doesn't verify toString
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
assertThat(this.ambiguousMeta)
.satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
.as("isSpy").isTrue())
.isSameAs(ctx.getBean("ambiguous1"));
assertThatException().isThrownBy(() -> ctx.getBean(StringBuilder.class))
.withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
assertThat(this.ambiguousMeta.toString()).isEqualTo("bean2");
assertThat(this.ambiguousMeta.length()).isEqualTo(5);
verify(this.ambiguousMeta, times(1)).length();
verifyNoMoreInteractions(this.ambiguousMeta); //mockito doesn't verify toString
}
@Test
void zeroCandidates() {
Class<?> caseClass = CaseNone.class;
@ -203,6 +289,20 @@ public class MockitoByTypeIntegrationTests { @@ -203,6 +289,20 @@ public class MockitoByTypeIntegrationTests {
ExampleService bean1() {
return new RealExampleService("Production hello");
}
@Bean("ambiguous1")
@Order(1)
@CustomQualifier
StringBuilder bean2() {
return new StringBuilder("bean2");
}
@Bean("ambiguous2")
@Order(2)
@Qualifier("prefer")
StringBuilder bean3() {
return new StringBuilder("bean3");
}
}
@Configuration

Loading…
Cancel
Save