Skip to content

Commit 74c811a

Browse files
jfrantziusJörg von Frantzius
andauthored
Make InjectMocks aware of generic types (#2923)
Fixes #2921 Co-authored-by: Jörg von Frantzius <[email protected]>
1 parent fc136e4 commit 74c811a

File tree

12 files changed

+469
-13
lines changed

12 files changed

+469
-13
lines changed

src/main/java/org/mockito/MockSettings.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mockito;
66

77
import java.io.Serializable;
8+
import java.lang.reflect.Type;
89

910
import org.mockito.exceptions.misusing.PotentialStubbingProblem;
1011
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
@@ -403,4 +404,11 @@ public interface MockSettings extends Serializable {
403404
* @since 4.8.0
404405
*/
405406
MockSettings mockMaker(String mockMaker);
407+
408+
/**
409+
* Specifies the generic type of the mock, preserving the information lost to Java type erasure.
410+
* @param genericTypeToMock
411+
* @return
412+
*/
413+
MockSettings genericTypeToMock(Type genericTypeToMock);
406414
}

src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public static Object processAnnotationForMock(
5757
mockSettings.mockMaker(annotation.mockMaker());
5858
}
5959

60+
mockSettings.genericTypeToMock(genericType.get());
61+
6062
// see @Mock answer default value
6163
mockSettings.defaultAnswer(annotation.answer());
6264

src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private static Object spyInstance(Field field, Object instance) {
8787
return Mockito.mock(
8888
instance.getClass(),
8989
withSettings()
90+
.genericTypeToMock(field.getGenericType())
9091
.spiedInstance(instance)
9192
.defaultAnswer(CALLS_REAL_METHODS)
9293
.name(field.getName()));
@@ -96,7 +97,10 @@ private static Object spyNewInstance(Object testInstance, Field field)
9697
throws InstantiationException, IllegalAccessException, InvocationTargetException {
9798
// TODO: Add mockMaker option for @Spy annotation (#2740)
9899
MockSettings settings =
99-
withSettings().defaultAnswer(CALLS_REAL_METHODS).name(field.getName());
100+
withSettings()
101+
.genericTypeToMock(field.getGenericType())
102+
.defaultAnswer(CALLS_REAL_METHODS)
103+
.name(field.getName());
100104
Class<?> type = field.getType();
101105
if (type.isInterface()) {
102106
return Mockito.mock(type, settings.useConstructor());

src/main/java/org/mockito/internal/configuration/injection/PropertyAndSetterInjection.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public boolean processInjection(
8181
injectMockCandidates(
8282
fieldClass,
8383
fieldInstanceNeedingInjection,
84+
injectMocksField,
8485
newMockSafeHashSet(mockCandidates));
8586
fieldClass = fieldClass.getSuperclass();
8687
}
@@ -100,32 +101,44 @@ private FieldInitializationReport initializeInjectMocksField(Field field, Object
100101
}
101102

102103
private boolean injectMockCandidates(
103-
Class<?> awaitingInjectionClazz, Object injectee, Set<Object> mocks) {
104+
Class<?> awaitingInjectionClazz,
105+
Object injectee,
106+
Field injectMocksField,
107+
Set<Object> mocks) {
104108
boolean injectionOccurred;
105109
List<Field> orderedCandidateInjecteeFields =
106110
orderedInstanceFieldsFrom(awaitingInjectionClazz);
107111
// pass 1
108112
injectionOccurred =
109113
injectMockCandidatesOnFields(
110-
mocks, injectee, false, orderedCandidateInjecteeFields);
114+
mocks, injectee, injectMocksField, false, orderedCandidateInjecteeFields);
111115
// pass 2
112116
injectionOccurred |=
113117
injectMockCandidatesOnFields(
114-
mocks, injectee, injectionOccurred, orderedCandidateInjecteeFields);
118+
mocks,
119+
injectee,
120+
injectMocksField,
121+
injectionOccurred,
122+
orderedCandidateInjecteeFields);
115123
return injectionOccurred;
116124
}
117125

118126
private boolean injectMockCandidatesOnFields(
119127
Set<Object> mocks,
120128
Object injectee,
129+
Field injectMocksField,
121130
boolean injectionOccurred,
122131
List<Field> orderedCandidateInjecteeFields) {
123132
for (Iterator<Field> it = orderedCandidateInjecteeFields.iterator(); it.hasNext(); ) {
124133
Field candidateField = it.next();
125134
Object injected =
126135
mockCandidateFilter
127136
.filterCandidate(
128-
mocks, candidateField, orderedCandidateInjecteeFields, injectee)
137+
mocks,
138+
candidateField,
139+
orderedCandidateInjecteeFields,
140+
injectee,
141+
injectMocksField)
129142
.thenInject();
130143
if (injected != null) {
131144
injectionOccurred |= true;

src/main/java/org/mockito/internal/configuration/injection/filter/MockCandidateFilter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ OngoingInjector filterCandidate(
1313
Collection<Object> mocks,
1414
Field candidateFieldToBeInjected,
1515
List<Field> allRemainingCandidateFields,
16-
Object injectee);
16+
Object injectee,
17+
Field injectMocksField);
1718
}

src/main/java/org/mockito/internal/configuration/injection/filter/NameBasedCandidateFilter.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public OngoingInjector filterCandidate(
2323
final Collection<Object> mocks,
2424
final Field candidateFieldToBeInjected,
2525
final List<Field> allRemainingCandidateFields,
26-
final Object injectee) {
26+
final Object injectee,
27+
final Field injectMocksField) {
2728
if (mocks.size() == 1
2829
&& anotherCandidateMatchesMockName(
2930
mocks, candidateFieldToBeInjected, allRemainingCandidateFields)) {
@@ -34,7 +35,8 @@ && anotherCandidateMatchesMockName(
3435
tooMany(mocks) ? selectMatchingName(mocks, candidateFieldToBeInjected) : mocks,
3536
candidateFieldToBeInjected,
3637
allRemainingCandidateFields,
37-
injectee);
38+
injectee,
39+
injectMocksField);
3840
}
3941

4042
private boolean tooMany(Collection<Object> mocks) {

src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public OngoingInjector filterCandidate(
2828
final Collection<Object> mocks,
2929
final Field candidateFieldToBeInjected,
3030
final List<Field> allRemainingCandidateFields,
31-
final Object injectee) {
31+
final Object injectee,
32+
final Field injectMocksField) {
3233
if (mocks.size() == 1) {
3334
final Object matchingMock = mocks.iterator().next();
3435

src/main/java/org/mockito/internal/configuration/injection/filter/TypeBasedCandidateFilter.java

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
package org.mockito.internal.configuration.injection.filter;
66

77
import java.lang.reflect.Field;
8+
import java.lang.reflect.ParameterizedType;
9+
import java.lang.reflect.Type;
10+
import java.lang.reflect.TypeVariable;
11+
import java.lang.reflect.WildcardType;
812
import java.util.ArrayList;
13+
import java.util.Arrays;
914
import java.util.Collection;
1015
import java.util.List;
1116

17+
import org.mockito.internal.util.MockUtil;
18+
1219
public class TypeBasedCandidateFilter implements MockCandidateFilter {
1320

1421
private final MockCandidateFilter next;
@@ -17,20 +24,123 @@ public TypeBasedCandidateFilter(MockCandidateFilter next) {
1724
this.next = next;
1825
}
1926

27+
protected boolean isCompatibleTypes(Type typeToMock, Type mockType, Field injectMocksField) {
28+
boolean result = false;
29+
if (typeToMock instanceof ParameterizedType && mockType instanceof ParameterizedType) {
30+
// ParameterizedType.equals() is documented as:
31+
// "Instances of classes that implement this interface must implement
32+
// an equals() method that equates any two instances that share the
33+
// same generic type declaration and have equal type parameters."
34+
// Unfortunately, e.g. Wildcard parameter "?" doesn't equal java.lang.String,
35+
// and e.g. Set doesn't equal TreeSet, so roll our own comparison if
36+
// ParameterizedTypeImpl.equals() returns false
37+
if (typeToMock.equals(mockType)) {
38+
result = true;
39+
} else {
40+
ParameterizedType genericTypeToMock = (ParameterizedType) typeToMock;
41+
ParameterizedType genericMockType = (ParameterizedType) mockType;
42+
Type[] actualTypeArguments = genericTypeToMock.getActualTypeArguments();
43+
Type[] actualTypeArguments2 = genericMockType.getActualTypeArguments();
44+
// Recurse on type parameters, so we properly test whether e.g. Wildcard bounds
45+
// have a match
46+
result =
47+
recurseOnTypeArguments(
48+
injectMocksField, actualTypeArguments, actualTypeArguments2);
49+
}
50+
} else if (typeToMock instanceof WildcardType) {
51+
WildcardType wildcardTypeToMock = (WildcardType) typeToMock;
52+
Type[] upperBounds = wildcardTypeToMock.getUpperBounds();
53+
result =
54+
Arrays.stream(upperBounds)
55+
.anyMatch(t -> isCompatibleTypes(t, mockType, injectMocksField));
56+
} else if (typeToMock instanceof Class && mockType instanceof Class) {
57+
result = ((Class) typeToMock).isAssignableFrom((Class) mockType);
58+
} // no need to check for GenericArrayType, as Mockito cannot mock this anyway
59+
60+
return result;
61+
}
62+
63+
private boolean recurseOnTypeArguments(
64+
Field injectMocksField, Type[] actualTypeArguments, Type[] actualTypeArguments2) {
65+
boolean isCompatible = true;
66+
for (int i = 0; i < actualTypeArguments.length; i++) {
67+
Type actualTypeArgument = actualTypeArguments[i];
68+
Type actualTypeArgument2 = actualTypeArguments2[i];
69+
if (actualTypeArgument instanceof TypeVariable) {
70+
TypeVariable<?> typeVariable = (TypeVariable<?>) actualTypeArgument;
71+
// this is a TypeVariable declared by the class under test that turned
72+
// up in one of its fields,
73+
// e.g. class ClassUnderTest<T1, T2> { List<T1> tList; Set<T2> tSet}
74+
// The TypeVariable`s actual type is declared by the field containing
75+
// the object under test, i.e. the field annotated with @InjectMocks
76+
// e.g. @InjectMocks ClassUnderTest<String, Integer> underTest = ..
77+
Type[] injectMocksFieldTypeParameters =
78+
((ParameterizedType) injectMocksField.getGenericType())
79+
.getActualTypeArguments();
80+
// Find index of given TypeVariable where it was defined, e.g. 0 for T1 in
81+
// ClassUnderTest<T1, T2>
82+
// (we're always able to find it, otherwise test class wouldn't have compiled))
83+
TypeVariable<?>[] genericTypeParameters =
84+
injectMocksField.getType().getTypeParameters();
85+
int variableIndex = -1;
86+
for (int i2 = 0; i2 < genericTypeParameters.length; i2++) {
87+
if (genericTypeParameters[i2].equals(typeVariable)) {
88+
variableIndex = i2;
89+
break;
90+
}
91+
}
92+
// now test whether actual type for the type variable is compatible, e.g. for
93+
// class ClassUnderTest<T1, T2> {..}
94+
// T1 would be the String in
95+
// ClassUnderTest<String, Integer> underTest = ..
96+
isCompatible &=
97+
isCompatibleTypes(
98+
injectMocksFieldTypeParameters[variableIndex],
99+
actualTypeArgument2,
100+
injectMocksField);
101+
} else {
102+
isCompatible &=
103+
isCompatibleTypes(
104+
actualTypeArgument, actualTypeArgument2, injectMocksField);
105+
}
106+
}
107+
return isCompatible;
108+
}
109+
20110
@Override
21111
public OngoingInjector filterCandidate(
22112
final Collection<Object> mocks,
23113
final Field candidateFieldToBeInjected,
24114
final List<Field> allRemainingCandidateFields,
25-
final Object injectee) {
115+
final Object injectee,
116+
final Field injectMocksField) {
26117
List<Object> mockTypeMatches = new ArrayList<>();
27118
for (Object mock : mocks) {
28119
if (candidateFieldToBeInjected.getType().isAssignableFrom(mock.getClass())) {
29-
mockTypeMatches.add(mock);
30-
}
120+
Type genericMockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
121+
Type genericType = candidateFieldToBeInjected.getGenericType();
122+
boolean bothHaveGenericTypeInfo = genericType != null && genericMockType != null;
123+
if (bothHaveGenericTypeInfo) {
124+
// be more specific if generic type information is available
125+
if (isCompatibleTypes(genericType, genericMockType, injectMocksField)) {
126+
mockTypeMatches.add(mock);
127+
} // else filter out mock, as generic types don't match
128+
} else {
129+
// field is assignable from mock class, but no generic type information
130+
// is available (can happen with programmatically created Mocks where no
131+
// genericTypeToMock was supplied)
132+
mockTypeMatches.add(mock);
133+
}
134+
} // else filter out mock
135+
// BTW mocks may contain Spy objects with their original class (seemingly before
136+
// being wrapped), and MockUtil.getMockSettings() throws exception for those
31137
}
32138

33139
return next.filterCandidate(
34-
mockTypeMatches, candidateFieldToBeInjected, allRemainingCandidateFields, injectee);
140+
mockTypeMatches,
141+
candidateFieldToBeInjected,
142+
allRemainingCandidateFields,
143+
injectee,
144+
injectMocksField);
35145
}
36146
}

src/main/java/org/mockito/internal/creation/MockSettingsImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static org.mockito.internal.util.collections.Sets.newSet;
1717

1818
import java.io.Serializable;
19+
import java.lang.reflect.Type;
1920
import java.util.ArrayList;
2021
import java.util.HashSet;
2122
import java.util.List;
@@ -260,6 +261,12 @@ public MockSettings mockMaker(String mockMaker) {
260261
return this;
261262
}
262263

264+
@Override
265+
public MockSettings genericTypeToMock(Type genericType) {
266+
this.genericTypeToMock = genericType;
267+
return this;
268+
}
269+
263270
private static <T> CreationSettings<T> validatedSettings(
264271
Class<T> typeToMock, CreationSettings<T> source) {
265272
MockCreationValidator validator = new MockCreationValidator();

src/main/java/org/mockito/internal/creation/settings/CreationSettings.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mockito.internal.creation.settings;
66

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

2728
protected Class<T> typeToMock;
29+
protected Type genericTypeToMock;
2830
protected Set<Class<?>> extraInterfaces = new LinkedHashSet<>();
2931
protected String name;
3032
protected Object spiedInstance;
@@ -54,6 +56,7 @@ public CreationSettings() {}
5456
public CreationSettings(CreationSettings copy) {
5557
// TODO can we have a reflection test here? We had a couple of bugs here in the past.
5658
this.typeToMock = copy.typeToMock;
59+
this.genericTypeToMock = copy.genericTypeToMock;
5760
this.extraInterfaces = copy.extraInterfaces;
5861
this.name = copy.name;
5962
this.spiedInstance = copy.spiedInstance;
@@ -82,6 +85,11 @@ public CreationSettings<T> setTypeToMock(Class<T> typeToMock) {
8285
return this;
8386
}
8487

88+
public CreationSettings<T> setGenericTypeToMock(Type genericTypeToMock) {
89+
this.genericTypeToMock = genericTypeToMock;
90+
return this;
91+
}
92+
8593
@Override
8694
public Set<Class<?>> getExtraInterfaces() {
8795
return extraInterfaces;
@@ -185,4 +193,9 @@ public Strictness getStrictness() {
185193
public String getMockMaker() {
186194
return mockMaker;
187195
}
196+
197+
@Override
198+
public Type getGenericTypeToMock() {
199+
return genericTypeToMock;
200+
}
188201
}

0 commit comments

Comments
 (0)