Update @MockBean to support generics

Update @MockBean and @SpyBean to support field generics. Prior to this
commit the following fields would fail with a "Duplicate mock
definition" exception:

  @MockBean
  private IdentityProvider<PasswordIdentity> passwordIdentityProvider;

  @MockBean
  private IdentityProvider<Oauth2Identity> oauth2IdentityProvider;

Fixes gh-6602
This commit is contained in:
Phillip Webb
2016-08-29 21:38:12 +01:00
parent 565ad79856
commit a985a5c861
11 changed files with 255 additions and 92 deletions

View File

@@ -18,7 +18,6 @@ package org.springframework.boot.test.mock.mockito;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -26,6 +25,7 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -82,15 +82,15 @@ class DefinitionsParser {
}
private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement element) {
Set<Class<?>> classesToMock = getOrDeduceClasses(element, annotation.value());
Assert.state(!classesToMock.isEmpty(),
"Unable to deduce class to mock from " + element);
Set<ResolvableType> typesToMock = getOrDeduceTypes(element, annotation.value());
Assert.state(!typesToMock.isEmpty(),
"Unable to deduce type to mock from " + element);
if (StringUtils.hasLength(annotation.name())) {
Assert.state(classesToMock.size() == 1,
Assert.state(typesToMock.size() == 1,
"The name attribute can only be used when mocking a single class");
}
for (Class<?> classToMock : classesToMock) {
MockDefinition definition = new MockDefinition(annotation.name(), classToMock,
for (ResolvableType typeToMock : typesToMock) {
MockDefinition definition = new MockDefinition(annotation.name(), typeToMock,
annotation.extraInterfaces(), annotation.answer(),
annotation.serializable(), annotation.reset(),
annotation.proxyTargetAware());
@@ -99,15 +99,15 @@ class DefinitionsParser {
}
private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element) {
Set<Class<?>> classesToSpy = getOrDeduceClasses(element, annotation.value());
Assert.state(!classesToSpy.isEmpty(),
"Unable to deduce class to spy from " + element);
Set<ResolvableType> typesToSpy = getOrDeduceTypes(element, annotation.value());
Assert.state(!typesToSpy.isEmpty(),
"Unable to deduce type to spy from " + element);
if (StringUtils.hasLength(annotation.name())) {
Assert.state(classesToSpy.size() == 1,
Assert.state(typesToSpy.size() == 1,
"The name attribute can only be used when spying a single class");
}
for (Class<?> classToSpy : classesToSpy) {
SpyDefinition definition = new SpyDefinition(annotation.name(), classToSpy,
for (ResolvableType typeToSpy : typesToSpy) {
SpyDefinition definition = new SpyDefinition(annotation.name(), typeToSpy,
annotation.reset(), annotation.proxyTargetAware());
addDefinition(element, definition, "spy");
}
@@ -123,13 +123,16 @@ class DefinitionsParser {
}
}
private Set<Class<?>> getOrDeduceClasses(AnnotatedElement element, Class<?>[] value) {
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
classes.addAll(Arrays.asList(value));
if (classes.isEmpty() && element instanceof Field) {
classes.add(((Field) element).getType());
private Set<ResolvableType> getOrDeduceTypes(AnnotatedElement element,
Class<?>[] value) {
Set<ResolvableType> types = new LinkedHashSet<ResolvableType>();
for (Class<?> type : value) {
types.add(ResolvableType.forClass(type));
}
return classes;
if (types.isEmpty() && element instanceof Field) {
types.add(ResolvableType.forField((Field) element));
}
return types;
}
public Set<Definition> getDefinitions() {

View File

@@ -26,6 +26,7 @@ import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -40,7 +41,7 @@ class MockDefinition extends Definition {
private static final int MULTIPLIER = 31;
private final Class<?> classToMock;
private final ResolvableType typeToMock;
private final Set<Class<?>> extraInterfaces;
@@ -49,15 +50,19 @@ class MockDefinition extends Definition {
private final boolean serializable;
MockDefinition(Class<?> classToMock) {
this(null, classToMock, null, null, false, null, true);
this(ResolvableType.forClass(classToMock));
}
MockDefinition(String name, Class<?> classToMock, Class<?>[] extraInterfaces,
MockDefinition(ResolvableType typeToMock) {
this(null, typeToMock, null, null, false, null, true);
}
MockDefinition(String name, ResolvableType typeToMock, Class<?>[] extraInterfaces,
Answers answer, boolean serializable, MockReset reset,
boolean proxyTargetAware) {
super(name, reset, proxyTargetAware);
Assert.notNull(classToMock, "ClassToMock must not be null");
this.classToMock = classToMock;
Assert.notNull(typeToMock, "TypeToMock must not be null");
this.typeToMock = typeToMock;
this.extraInterfaces = asClassSet(extraInterfaces);
this.answer = (answer != null ? answer : Answers.RETURNS_DEFAULTS);
this.serializable = serializable;
@@ -72,11 +77,11 @@ class MockDefinition extends Definition {
}
/**
* Return the class that should be mocked.
* Return the type that should be mocked.
* @return the class to mock; never {@code null}
*/
public Class<?> getClassToMock() {
return this.classToMock;
public ResolvableType getTypeToMock() {
return this.typeToMock;
}
/**
@@ -106,7 +111,7 @@ class MockDefinition extends Definition {
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.classToMock);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToMock);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer);
result = MULTIPLIER * result + (this.serializable ? 1231 : 1237);
@@ -123,7 +128,7 @@ class MockDefinition extends Definition {
}
MockDefinition other = (MockDefinition) obj;
boolean result = super.equals(obj);
result &= ObjectUtils.nullSafeEquals(this.classToMock, other.classToMock);
result &= ObjectUtils.nullSafeEquals(this.typeToMock, other.typeToMock);
result &= ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces);
result &= ObjectUtils.nullSafeEquals(this.answer, other.answer);
result &= this.serializable == other.serializable;
@@ -133,7 +138,7 @@ class MockDefinition extends Definition {
@Override
public String toString() {
return new ToStringCreator(this).append("name", getName())
.append("classToMock", this.classToMock)
.append("typeToMock", this.typeToMock)
.append("extraInterfaces", this.extraInterfaces)
.append("answer", this.answer).append("serializable", this.serializable)
.append("reset", getReset()).toString();
@@ -156,7 +161,7 @@ class MockDefinition extends Definition {
if (this.serializable) {
settings.serializable();
}
return (T) Mockito.mock(this.classToMock, settings);
return (T) Mockito.mock(this.typeToMock.resolve(), settings);
}
private Answer<?> getAnswer(Answers answer) {

View File

@@ -54,6 +54,7 @@ import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.Conventions;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -190,8 +191,8 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
private RootBeanDefinition createBeanDefinition(MockDefinition mockDefinition) {
RootBeanDefinition definition = new RootBeanDefinition(
mockDefinition.getClassToMock());
definition.setTargetType(mockDefinition.getClassToMock());
mockDefinition.getTypeToMock().resolve());
definition.setTargetType(mockDefinition.getTypeToMock());
definition.setFactoryBeanName(BEAN_NAME);
definition.setFactoryMethodName("createMock");
definition.getConstructorArgumentValues().addIndexedArgumentValue(0,
@@ -216,23 +217,22 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
return mockDefinition.getName();
}
String[] existingBeans = getExistingBeans(beanFactory,
mockDefinition.getClassToMock());
mockDefinition.getTypeToMock());
if (ObjectUtils.isEmpty(existingBeans)) {
return this.beanNameGenerator.generateBeanName(beanDefinition, registry);
}
if (existingBeans.length == 1) {
return existingBeans[0];
}
throw new IllegalStateException("Unable to register mock bean "
+ mockDefinition.getClassToMock().getName()
+ " expected a single existing bean to replace but found "
+ new TreeSet<String>(Arrays.asList(existingBeans)));
throw new IllegalStateException(
"Unable to register mock bean " + mockDefinition.getTypeToMock()
+ " expected a single existing bean to replace but found "
+ new TreeSet<String>(Arrays.asList(existingBeans)));
}
private void registerSpy(ConfigurableListableBeanFactory beanFactory,
BeanDefinitionRegistry registry, SpyDefinition definition, Field field) {
String[] existingBeans = getExistingBeans(beanFactory,
definition.getClassToSpy());
String[] existingBeans = getExistingBeans(beanFactory, definition.getTypeToSpy());
if (ObjectUtils.isEmpty(existingBeans)) {
createSpy(registry, definition, field);
}
@@ -242,13 +242,14 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
}
private String[] getExistingBeans(ConfigurableListableBeanFactory beanFactory,
Class<?> type) {
ResolvableType type) {
Set<String> beans = new LinkedHashSet<String>(
Arrays.asList(beanFactory.getBeanNamesForType(type)));
String resolvedTypeName = type.resolve(Object.class).getName();
for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class)) {
beanName = BeanFactoryUtils.transformedBeanName(beanName);
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
if (type.getName()
if (resolvedTypeName
.equals(beanDefinition.getAttribute(FACTORY_BEAN_OBJECT_TYPE))) {
beans.add(beanName);
}
@@ -273,7 +274,7 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
private void createSpy(BeanDefinitionRegistry registry, SpyDefinition definition,
Field field) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(
definition.getClassToSpy());
definition.getTypeToSpy().resolve());
String beanName = this.beanNameGenerator.generateBeanName(beanDefinition,
registry);
registry.registerBeanDefinition(beanName, beanDefinition);
@@ -283,7 +284,7 @@ public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAda
private void registerSpies(SpyDefinition definition, Field field,
String[] existingBeans) {
Assert.state(field == null || existingBeans.length == 1,
"Unable to register spy bean " + definition.getClassToSpy().getName()
"Unable to register spy bean " + definition.getTypeToSpy()
+ " expected a single existing bean to replace but found "
+ new TreeSet<String>(Arrays.asList(existingBeans)));
for (String beanName : existingBeans) {

View File

@@ -100,8 +100,8 @@ public @interface SpyBean {
* The classes to spy. Each class specified here will result in a spy being applied.
* Classes can be omitted when the annotation is used on a field.
* <p>
* When {@code @SpyBean} also defines a {@code name} this attribute can only contain
* a single value.
* When {@code @SpyBean} also defines a {@code name} this attribute can only contain a
* single value.
* <p>
* If this is the only specified attribute consider using the {@code value} alias
* instead.

View File

@@ -20,6 +20,7 @@ import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.internal.util.MockUtil;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -36,24 +37,24 @@ class SpyDefinition extends Definition {
private static final int MULTIPLIER = 31;
private final Class<?> classToSpy;
private final ResolvableType typeToSpy;
SpyDefinition(String name, Class<?> classToSpy, MockReset reset,
SpyDefinition(String name, ResolvableType typeToSpy, MockReset reset,
boolean proxyTargetAware) {
super(name, reset, proxyTargetAware);
Assert.notNull(classToSpy, "ClassToSpy must not be null");
this.classToSpy = classToSpy;
Assert.notNull(typeToSpy, "TypeToSpy must not be null");
this.typeToSpy = typeToSpy;
}
public Class<?> getClassToSpy() {
return this.classToSpy;
public ResolvableType getTypeToSpy() {
return this.typeToSpy;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.classToSpy);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToSpy);
return result;
}
@@ -67,14 +68,14 @@ class SpyDefinition extends Definition {
}
SpyDefinition other = (SpyDefinition) obj;
boolean result = super.equals(obj);
result &= ObjectUtils.nullSafeEquals(this.classToSpy, other.classToSpy);
result &= ObjectUtils.nullSafeEquals(this.typeToSpy, other.typeToSpy);
return result;
}
@Override
public String toString() {
return new ToStringCreator(this).append("name", getName())
.append("classToSpy", this.classToSpy).append("reset", getReset())
.append("typeToSpy", this.typeToSpy).append("reset", getReset())
.toString();
}
@@ -85,7 +86,7 @@ class SpyDefinition extends Definition {
@SuppressWarnings("unchecked")
public <T> T createSpy(String name, Object instance) {
Assert.notNull(instance, "Instance must not be null");
Assert.isInstanceOf(this.classToSpy, instance);
Assert.isInstanceOf(this.typeToSpy.resolve(), instance);
if (this.mockUtil.isSpy(instance)) {
return (T) instance;
}