Skip to content

Commit

Permalink
Make InjectMocks aware of generic types (#2923)
Browse files Browse the repository at this point in the history
Fixes #2921

Co-authored-by: Jörg von Frantzius <joerg.frantzius@aperto.com>
  • Loading branch information
jfrantzius and Jörg von Frantzius committed Mar 9, 2023
1 parent fc136e4 commit 74c811a
Show file tree
Hide file tree
Showing 12 changed files with 469 additions and 13 deletions.
8 changes: 8 additions & 0 deletions src/main/java/org/mockito/MockSettings.java
Expand Up @@ -5,6 +5,7 @@
package org.mockito;

import java.io.Serializable;
import java.lang.reflect.Type;

import org.mockito.exceptions.misusing.PotentialStubbingProblem;
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
Expand Down Expand Up @@ -403,4 +404,11 @@ public interface MockSettings extends Serializable {
* @since 4.8.0
*/
MockSettings mockMaker(String mockMaker);

/**
* Specifies the generic type of the mock, preserving the information lost to Java type erasure.
* @param genericTypeToMock
* @return
*/
MockSettings genericTypeToMock(Type genericTypeToMock);
}
Expand Up @@ -57,6 +57,8 @@ public static Object processAnnotationForMock(
mockSettings.mockMaker(annotation.mockMaker());
}

mockSettings.genericTypeToMock(genericType.get());

// see @Mock answer default value
mockSettings.defaultAnswer(annotation.answer());

Expand Down
Expand Up @@ -87,6 +87,7 @@ private static Object spyInstance(Field field, Object instance) {
return Mockito.mock(
instance.getClass(),
withSettings()
.genericTypeToMock(field.getGenericType())
.spiedInstance(instance)
.defaultAnswer(CALLS_REAL_METHODS)
.name(field.getName()));
Expand All @@ -96,7 +97,10 @@ private static Object spyNewInstance(Object testInstance, Field field)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
// TODO: Add mockMaker option for @Spy annotation (#2740)
MockSettings settings =
withSettings().defaultAnswer(CALLS_REAL_METHODS).name(field.getName());
withSettings()
.genericTypeToMock(field.getGenericType())
.defaultAnswer(CALLS_REAL_METHODS)
.name(field.getName());
Class<?> type = field.getType();
if (type.isInterface()) {
return Mockito.mock(type, settings.useConstructor());
Expand Down
Expand Up @@ -81,6 +81,7 @@ public boolean processInjection(
injectMockCandidates(
fieldClass,
fieldInstanceNeedingInjection,
injectMocksField,
newMockSafeHashSet(mockCandidates));
fieldClass = fieldClass.getSuperclass();
}
Expand All @@ -100,32 +101,44 @@ private FieldInitializationReport initializeInjectMocksField(Field field, Object
}

private boolean injectMockCandidates(
Class<?> awaitingInjectionClazz, Object injectee, Set<Object> mocks) {
Class<?> awaitingInjectionClazz,
Object injectee,
Field injectMocksField,
Set<Object> mocks) {
boolean injectionOccurred;
List<Field> orderedCandidateInjecteeFields =
orderedInstanceFieldsFrom(awaitingInjectionClazz);
// pass 1
injectionOccurred =
injectMockCandidatesOnFields(
mocks, injectee, false, orderedCandidateInjecteeFields);
mocks, injectee, injectMocksField, false, orderedCandidateInjecteeFields);
// pass 2
injectionOccurred |=
injectMockCandidatesOnFields(
mocks, injectee, injectionOccurred, orderedCandidateInjecteeFields);
mocks,
injectee,
injectMocksField,
injectionOccurred,
orderedCandidateInjecteeFields);
return injectionOccurred;
}

private boolean injectMockCandidatesOnFields(
Set<Object> mocks,
Object injectee,
Field injectMocksField,
boolean injectionOccurred,
List<Field> orderedCandidateInjecteeFields) {
for (Iterator<Field> it = orderedCandidateInjecteeFields.iterator(); it.hasNext(); ) {
Field candidateField = it.next();
Object injected =
mockCandidateFilter
.filterCandidate(
mocks, candidateField, orderedCandidateInjecteeFields, injectee)
mocks,
candidateField,
orderedCandidateInjecteeFields,
injectee,
injectMocksField)
.thenInject();
if (injected != null) {
injectionOccurred |= true;
Expand Down
Expand Up @@ -13,5 +13,6 @@ OngoingInjector filterCandidate(
Collection<Object> mocks,
Field candidateFieldToBeInjected,
List<Field> allRemainingCandidateFields,
Object injectee);
Object injectee,
Field injectMocksField);
}
Expand Up @@ -23,7 +23,8 @@ public OngoingInjector filterCandidate(
final Collection<Object> mocks,
final Field candidateFieldToBeInjected,
final List<Field> allRemainingCandidateFields,
final Object injectee) {
final Object injectee,
final Field injectMocksField) {
if (mocks.size() == 1
&& anotherCandidateMatchesMockName(
mocks, candidateFieldToBeInjected, allRemainingCandidateFields)) {
Expand All @@ -34,7 +35,8 @@ && anotherCandidateMatchesMockName(
tooMany(mocks) ? selectMatchingName(mocks, candidateFieldToBeInjected) : mocks,
candidateFieldToBeInjected,
allRemainingCandidateFields,
injectee);
injectee,
injectMocksField);
}

private boolean tooMany(Collection<Object> mocks) {
Expand Down
Expand Up @@ -28,7 +28,8 @@ public OngoingInjector filterCandidate(
final Collection<Object> mocks,
final Field candidateFieldToBeInjected,
final List<Field> allRemainingCandidateFields,
final Object injectee) {
final Object injectee,
final Field injectMocksField) {
if (mocks.size() == 1) {
final Object matchingMock = mocks.iterator().next();

Expand Down
Expand Up @@ -5,10 +5,17 @@
package org.mockito.internal.configuration.injection.filter;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.mockito.internal.util.MockUtil;

public class TypeBasedCandidateFilter implements MockCandidateFilter {

private final MockCandidateFilter next;
Expand All @@ -17,20 +24,123 @@ public TypeBasedCandidateFilter(MockCandidateFilter next) {
this.next = next;
}

protected boolean isCompatibleTypes(Type typeToMock, Type mockType, Field injectMocksField) {
boolean result = false;
if (typeToMock instanceof ParameterizedType && mockType instanceof ParameterizedType) {
// ParameterizedType.equals() is documented as:
// "Instances of classes that implement this interface must implement
// an equals() method that equates any two instances that share the
// same generic type declaration and have equal type parameters."
// Unfortunately, e.g. Wildcard parameter "?" doesn't equal java.lang.String,
// and e.g. Set doesn't equal TreeSet, so roll our own comparison if
// ParameterizedTypeImpl.equals() returns false
if (typeToMock.equals(mockType)) {
result = true;
} else {
ParameterizedType genericTypeToMock = (ParameterizedType) typeToMock;
ParameterizedType genericMockType = (ParameterizedType) mockType;
Type[] actualTypeArguments = genericTypeToMock.getActualTypeArguments();
Type[] actualTypeArguments2 = genericMockType.getActualTypeArguments();
// Recurse on type parameters, so we properly test whether e.g. Wildcard bounds
// have a match
result =
recurseOnTypeArguments(
injectMocksField, actualTypeArguments, actualTypeArguments2);
}
} else if (typeToMock instanceof WildcardType) {
WildcardType wildcardTypeToMock = (WildcardType) typeToMock;
Type[] upperBounds = wildcardTypeToMock.getUpperBounds();
result =
Arrays.stream(upperBounds)
.anyMatch(t -> isCompatibleTypes(t, mockType, injectMocksField));
} else if (typeToMock instanceof Class && mockType instanceof Class) {
result = ((Class) typeToMock).isAssignableFrom((Class) mockType);
} // no need to check for GenericArrayType, as Mockito cannot mock this anyway

return result;
}

private boolean recurseOnTypeArguments(
Field injectMocksField, Type[] actualTypeArguments, Type[] actualTypeArguments2) {
boolean isCompatible = true;
for (int i = 0; i < actualTypeArguments.length; i++) {
Type actualTypeArgument = actualTypeArguments[i];
Type actualTypeArgument2 = actualTypeArguments2[i];
if (actualTypeArgument instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) actualTypeArgument;
// this is a TypeVariable declared by the class under test that turned
// up in one of its fields,
// e.g. class ClassUnderTest<T1, T2> { List<T1> tList; Set<T2> tSet}
// The TypeVariable`s actual type is declared by the field containing
// the object under test, i.e. the field annotated with @InjectMocks
// e.g. @InjectMocks ClassUnderTest<String, Integer> underTest = ..
Type[] injectMocksFieldTypeParameters =
((ParameterizedType) injectMocksField.getGenericType())
.getActualTypeArguments();
// Find index of given TypeVariable where it was defined, e.g. 0 for T1 in
// ClassUnderTest<T1, T2>
// (we're always able to find it, otherwise test class wouldn't have compiled))
TypeVariable<?>[] genericTypeParameters =
injectMocksField.getType().getTypeParameters();
int variableIndex = -1;
for (int i2 = 0; i2 < genericTypeParameters.length; i2++) {
if (genericTypeParameters[i2].equals(typeVariable)) {
variableIndex = i2;
break;
}
}
// now test whether actual type for the type variable is compatible, e.g. for
// class ClassUnderTest<T1, T2> {..}
// T1 would be the String in
// ClassUnderTest<String, Integer> underTest = ..
isCompatible &=
isCompatibleTypes(
injectMocksFieldTypeParameters[variableIndex],
actualTypeArgument2,
injectMocksField);
} else {
isCompatible &=
isCompatibleTypes(
actualTypeArgument, actualTypeArgument2, injectMocksField);
}
}
return isCompatible;
}

@Override
public OngoingInjector filterCandidate(
final Collection<Object> mocks,
final Field candidateFieldToBeInjected,
final List<Field> allRemainingCandidateFields,
final Object injectee) {
final Object injectee,
final Field injectMocksField) {
List<Object> mockTypeMatches = new ArrayList<>();
for (Object mock : mocks) {
if (candidateFieldToBeInjected.getType().isAssignableFrom(mock.getClass())) {
mockTypeMatches.add(mock);
}
Type genericMockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
Type genericType = candidateFieldToBeInjected.getGenericType();
boolean bothHaveGenericTypeInfo = genericType != null && genericMockType != null;
if (bothHaveGenericTypeInfo) {
// be more specific if generic type information is available
if (isCompatibleTypes(genericType, genericMockType, injectMocksField)) {
mockTypeMatches.add(mock);
} // else filter out mock, as generic types don't match
} else {
// field is assignable from mock class, but no generic type information
// is available (can happen with programmatically created Mocks where no
// genericTypeToMock was supplied)
mockTypeMatches.add(mock);
}
} // else filter out mock
// BTW mocks may contain Spy objects with their original class (seemingly before
// being wrapped), and MockUtil.getMockSettings() throws exception for those
}

return next.filterCandidate(
mockTypeMatches, candidateFieldToBeInjected, allRemainingCandidateFields, injectee);
mockTypeMatches,
candidateFieldToBeInjected,
allRemainingCandidateFields,
injectee,
injectMocksField);
}
}
Expand Up @@ -16,6 +16,7 @@
import static org.mockito.internal.util.collections.Sets.newSet;

import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -260,6 +261,12 @@ public MockSettings mockMaker(String mockMaker) {
return this;
}

@Override
public MockSettings genericTypeToMock(Type genericType) {
this.genericTypeToMock = genericType;
return this;
}

private static <T> CreationSettings<T> validatedSettings(
Class<T> typeToMock, CreationSettings<T> source) {
MockCreationValidator validator = new MockCreationValidator();
Expand Down
Expand Up @@ -5,6 +5,7 @@
package org.mockito.internal.creation.settings;

import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
Expand All @@ -25,6 +26,7 @@ public class CreationSettings<T> implements MockCreationSettings<T>, Serializabl
private static final long serialVersionUID = -6789800638070123629L;

protected Class<T> typeToMock;
protected Type genericTypeToMock;
protected Set<Class<?>> extraInterfaces = new LinkedHashSet<>();
protected String name;
protected Object spiedInstance;
Expand Down Expand Up @@ -54,6 +56,7 @@ public CreationSettings() {}
public CreationSettings(CreationSettings copy) {
// TODO can we have a reflection test here? We had a couple of bugs here in the past.
this.typeToMock = copy.typeToMock;
this.genericTypeToMock = copy.genericTypeToMock;
this.extraInterfaces = copy.extraInterfaces;
this.name = copy.name;
this.spiedInstance = copy.spiedInstance;
Expand Down Expand Up @@ -82,6 +85,11 @@ public CreationSettings<T> setTypeToMock(Class<T> typeToMock) {
return this;
}

public CreationSettings<T> setGenericTypeToMock(Type genericTypeToMock) {
this.genericTypeToMock = genericTypeToMock;
return this;
}

@Override
public Set<Class<?>> getExtraInterfaces() {
return extraInterfaces;
Expand Down Expand Up @@ -185,4 +193,9 @@ public Strictness getStrictness() {
public String getMockMaker() {
return mockMaker;
}

@Override
public Type getGenericTypeToMock() {
return genericTypeToMock;
}
}
6 changes: 6 additions & 0 deletions src/main/java/org/mockito/mock/MockCreationSettings.java
Expand Up @@ -4,6 +4,7 @@
*/
package org.mockito.mock;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Set;

Expand All @@ -27,6 +28,11 @@ public interface MockCreationSettings<T> {
*/
Class<T> getTypeToMock();

/**
* The generic type of the mock, if any.
*/
Type getGenericTypeToMock();

/**
* the extra interfaces the mock object should implement.
*/
Expand Down

0 comments on commit 74c811a

Please sign in to comment.