Support Qualifiers on MockBean and SpyBean
Previously, if an injection point used a qualifier, `MockBean` and
`SpyBean` couldn't be used to mock/spy it as there was no way to
specify that qualifier information.
This commit now detects qualifier information on the injection point
and associate it with the created `BeanDefintion`. If one wants to
mock a bean that is qualified with `@Qualifier("foo")`, the definition
of the mock should be as follows:
```
public class MyTest {
@MockBean
@Qualifier("foo")
private ExampleService service;
}
```
As a side effect, it is now possible to mock a service by type even if
there are multiple instances of that type in the application context. The
provided qualifier information is used to determine the right candidate
and the proper bean definition is replaced accordingly.
Closes gh-6753
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.boot.test.mock.mockito;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -28,18 +30,29 @@ abstract class Definition {
|
||||
|
||||
private static final int MULTIPLIER = 31;
|
||||
|
||||
private final AnnotatedElement element;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final MockReset reset;
|
||||
|
||||
private final boolean proxyTargetAware;
|
||||
|
||||
Definition(String name, MockReset reset, boolean proxyTargetAware) {
|
||||
Definition(AnnotatedElement element, String name, MockReset reset, boolean proxyTargetAware) {
|
||||
this.element = element;
|
||||
this.name = name;
|
||||
this.reset = (reset != null ? reset : MockReset.AFTER);
|
||||
this.proxyTargetAware = proxyTargetAware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link AnnotatedElement} that holds this definition.
|
||||
* @return the element that defines this definition or {@code null}
|
||||
*/
|
||||
public AnnotatedElement getElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name for bean.
|
||||
* @return the name or {@code null}
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.springframework.util.StringUtils;
|
||||
* class.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class DefinitionsParser {
|
||||
|
||||
@@ -90,10 +91,10 @@ class DefinitionsParser {
|
||||
"The name attribute can only be used when mocking a single class");
|
||||
}
|
||||
for (ResolvableType typeToMock : typesToMock) {
|
||||
MockDefinition definition = new MockDefinition(annotation.name(), typeToMock,
|
||||
annotation.extraInterfaces(), annotation.answer(),
|
||||
MockDefinition definition = new MockDefinition(element, annotation.name(),
|
||||
typeToMock, annotation.extraInterfaces(), annotation.answer(),
|
||||
annotation.serializable(), annotation.reset());
|
||||
addDefinition(element, definition, "mock");
|
||||
addDefinition(definition, "mock");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,16 +107,17 @@ class DefinitionsParser {
|
||||
"The name attribute can only be used when spying a single class");
|
||||
}
|
||||
for (ResolvableType typeToSpy : typesToSpy) {
|
||||
SpyDefinition definition = new SpyDefinition(annotation.name(), typeToSpy,
|
||||
annotation.reset(), annotation.proxyTargetAware());
|
||||
addDefinition(element, definition, "spy");
|
||||
SpyDefinition definition = new SpyDefinition(element, annotation.name(),
|
||||
typeToSpy, annotation.reset(), annotation.proxyTargetAware());
|
||||
addDefinition(definition, "spy");
|
||||
}
|
||||
}
|
||||
|
||||
private void addDefinition(AnnotatedElement element, Definition definition,
|
||||
private void addDefinition(Definition definition,
|
||||
String type) {
|
||||
boolean isNewDefinition = this.definitions.add(definition);
|
||||
Assert.state(isNewDefinition, "Duplicate " + type + " definition " + definition);
|
||||
AnnotatedElement element = definition.getElement();
|
||||
if (element instanceof Field) {
|
||||
Field field = (Field) element;
|
||||
this.definitionFields.put(definition, field);
|
||||
|
||||
@@ -67,6 +67,18 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
* If there is more than one bean of the requested type, qualifier metadata must be
|
||||
* specified at field level: <pre class="code">
|
||||
* @RunWith(SpringRunner.class)
|
||||
* public class ExampleTests {
|
||||
*
|
||||
* @MockBean
|
||||
* @Qualifier("example")
|
||||
* private ExampleService service;
|
||||
*
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* This annotation is {@code @Repeatable} and may be specified multiple times when working
|
||||
* with Java 8 or contained within an {@link MockBeans @MockBeans} annotation.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.test.mock.mockito;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -49,17 +50,9 @@ class MockDefinition extends Definition {
|
||||
|
||||
private final boolean serializable;
|
||||
|
||||
MockDefinition(Class<?> classToMock) {
|
||||
this(ResolvableType.forClass(classToMock));
|
||||
}
|
||||
|
||||
MockDefinition(ResolvableType typeToMock) {
|
||||
this(null, typeToMock, null, null, false, null);
|
||||
}
|
||||
|
||||
MockDefinition(String name, ResolvableType typeToMock, Class<?>[] extraInterfaces,
|
||||
MockDefinition(AnnotatedElement element, String name, ResolvableType typeToMock, Class<?>[] extraInterfaces,
|
||||
Answers answer, boolean serializable, MockReset reset) {
|
||||
super(name, reset, false);
|
||||
super(element, name, reset, false);
|
||||
Assert.notNull(typeToMock, "TypeToMock must not be null");
|
||||
this.typeToMock = typeToMock;
|
||||
this.extraInterfaces = asClassSet(extraInterfaces);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.test.mock.mockito;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -43,6 +44,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
|
||||
import org.springframework.beans.factory.config.DependencyDescriptor;
|
||||
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
@@ -72,6 +74,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
|
||||
@@ -206,6 +209,10 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
|
||||
definition.setFactoryMethodName("createMock");
|
||||
definition.getConstructorArgumentValues().addIndexedArgumentValue(0,
|
||||
mockDefinition);
|
||||
AnnotatedElement element = mockDefinition.getElement();
|
||||
if (element instanceof Field) {
|
||||
definition.setQualifiedElement(element);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
@@ -225,8 +232,7 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
|
||||
if (StringUtils.hasLength(mockDefinition.getName())) {
|
||||
return mockDefinition.getName();
|
||||
}
|
||||
String[] existingBeans = getExistingBeans(beanFactory,
|
||||
mockDefinition.getTypeToMock());
|
||||
String[] existingBeans = findCandidateBeans(beanFactory, mockDefinition);
|
||||
if (ObjectUtils.isEmpty(existingBeans)) {
|
||||
return this.beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
}
|
||||
@@ -235,7 +241,7 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Unable to register mock bean " + mockDefinition.getTypeToMock()
|
||||
+ " expected a single existing bean to replace but found "
|
||||
+ " expected a single matching bean to replace but found "
|
||||
+ new TreeSet<String>(Arrays.asList(existingBeans)));
|
||||
}
|
||||
|
||||
@@ -250,6 +256,24 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
|
||||
}
|
||||
}
|
||||
|
||||
private String[] findCandidateBeans(ConfigurableListableBeanFactory beanFactory,
|
||||
MockDefinition mockDefinition) {
|
||||
String[] beans = getExistingBeans(beanFactory, mockDefinition.getTypeToMock());
|
||||
// Attempt to filter using qualifiers
|
||||
if (beans.length > 1 && mockDefinition.getElement() instanceof Field) {
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(
|
||||
(Field) mockDefinition.getElement(), true);
|
||||
Set<String> candidates = new LinkedHashSet<String>();
|
||||
for (String bean : beans) {
|
||||
if (beanFactory.isAutowireCandidate(bean, descriptor)) {
|
||||
candidates.add(bean);
|
||||
}
|
||||
}
|
||||
return candidates.toArray(new String[candidates.size()]);
|
||||
}
|
||||
return beans;
|
||||
}
|
||||
|
||||
private String[] getExistingBeans(ConfigurableListableBeanFactory beanFactory,
|
||||
ResolvableType type) {
|
||||
Set<String> beans = new LinkedHashSet<String>(
|
||||
|
||||
@@ -67,6 +67,18 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
* If there is more than one bean of the requested type, qualifier metadata must be
|
||||
* specified at field level: <pre class="code">
|
||||
* @RunWith(SpringRunner.class)
|
||||
* public class ExampleTests {
|
||||
*
|
||||
* @SpyBean
|
||||
* @Qualifier("example")
|
||||
* private ExampleService service;
|
||||
*
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* This annotation is {@code @Repeatable} and may be specified multiple times when working
|
||||
* with Java 8 or contained within a {@link SpyBeans @SpyBeans} annotation.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.boot.test.mock.mockito;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
|
||||
import org.mockito.MockSettings;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.internal.util.MockUtil;
|
||||
@@ -39,9 +41,9 @@ class SpyDefinition extends Definition {
|
||||
|
||||
private final ResolvableType typeToSpy;
|
||||
|
||||
SpyDefinition(String name, ResolvableType typeToSpy, MockReset reset,
|
||||
boolean proxyTargetAware) {
|
||||
super(name, reset, proxyTargetAware);
|
||||
SpyDefinition(AnnotatedElement element, String name, ResolvableType typeToSpy,
|
||||
MockReset reset, boolean proxyTargetAware) {
|
||||
super(element, name, reset, proxyTargetAware);
|
||||
Assert.notNull(typeToSpy, "TypeToSpy must not be null");
|
||||
this.typeToSpy = typeToSpy;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user