Skip to content

Commit 84a5eee

Browse files
jfrantziusJörg von Frantzius
andauthored
Fix ClassCastException regression with parameterized types with InjectMocks (#2962)
Fixes #2958 Co-authored-by: Jörg von Frantzius <joerg.frantzius@aperto.com>
1 parent d65bba7 commit 84a5eee

File tree

2 files changed

+166
-46
lines changed

2 files changed

+166
-46
lines changed

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

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Arrays;
1616
import java.util.Collection;
1717
import java.util.List;
18+
import java.util.stream.Stream;
1819

1920
import org.mockito.internal.util.MockUtil;
2021

@@ -28,26 +29,38 @@ public TypeBasedCandidateFilter(MockCandidateFilter next) {
2829

2930
protected boolean isCompatibleTypes(Type typeToMock, Type mockType, Field injectMocksField) {
3031
boolean result = false;
31-
if (typeToMock instanceof ParameterizedType && mockType instanceof ParameterizedType) {
32-
// ParameterizedType.equals() is documented as:
33-
// "Instances of classes that implement this interface must implement
34-
// an equals() method that equates any two instances that share the
35-
// same generic type declaration and have equal type parameters."
36-
// Unfortunately, e.g. Wildcard parameter "?" doesn't equal java.lang.String,
37-
// and e.g. Set doesn't equal TreeSet, so roll our own comparison if
38-
// ParameterizedTypeImpl.equals() returns false
39-
if (typeToMock.equals(mockType)) {
40-
result = true;
32+
if (typeToMock instanceof ParameterizedType) {
33+
if (mockType instanceof ParameterizedType) {
34+
// ParameterizedType.equals() is documented as:
35+
// "Instances of classes that implement this interface must implement
36+
// an equals() method that equates any two instances that share the
37+
// same generic type declaration and have equal type parameters."
38+
// Unfortunately, e.g. Wildcard parameter "?" doesn't equal java.lang.String,
39+
// and e.g. Set doesn't equal TreeSet, so roll our own comparison if
40+
// ParameterizedTypeImpl.equals() returns false
41+
if (typeToMock.equals(mockType)) {
42+
result = true;
43+
} else {
44+
ParameterizedType genericTypeToMock = (ParameterizedType) typeToMock;
45+
ParameterizedType genericMockType = (ParameterizedType) mockType;
46+
Type[] actualTypeArguments = genericTypeToMock.getActualTypeArguments();
47+
Type[] actualTypeArguments2 = genericMockType.getActualTypeArguments();
48+
// Recurse on type parameters, so we properly test whether e.g. Wildcard bounds
49+
// have a match
50+
result =
51+
recurseOnTypeArguments(
52+
injectMocksField, actualTypeArguments, actualTypeArguments2);
53+
}
4154
} else {
42-
ParameterizedType genericTypeToMock = (ParameterizedType) typeToMock;
43-
ParameterizedType genericMockType = (ParameterizedType) mockType;
44-
Type[] actualTypeArguments = genericTypeToMock.getActualTypeArguments();
45-
Type[] actualTypeArguments2 = genericMockType.getActualTypeArguments();
46-
// Recurse on type parameters, so we properly test whether e.g. Wildcard bounds
47-
// have a match
55+
// mockType is a non-parameterized Class, i.e. a concrete class.
56+
// so walk concrete class' type hierarchy
57+
Class<?> concreteMockClass = (Class<?>) mockType;
58+
Stream<Type> mockSuperTypes = getSuperTypes(concreteMockClass);
4859
result =
49-
recurseOnTypeArguments(
50-
injectMocksField, actualTypeArguments, actualTypeArguments2);
60+
mockSuperTypes.anyMatch(
61+
mockSuperType ->
62+
isCompatibleTypes(
63+
typeToMock, mockSuperType, injectMocksField));
5164
}
5265
} else if (typeToMock instanceof WildcardType) {
5366
WildcardType wildcardTypeToMock = (WildcardType) typeToMock;
@@ -56,12 +69,19 @@ protected boolean isCompatibleTypes(Type typeToMock, Type mockType, Field inject
5669
Arrays.stream(upperBounds)
5770
.anyMatch(t -> isCompatibleTypes(t, mockType, injectMocksField));
5871
} else if (typeToMock instanceof Class && mockType instanceof Class) {
59-
result = ((Class) typeToMock).isAssignableFrom((Class) mockType);
72+
result = ((Class<?>) typeToMock).isAssignableFrom((Class<?>) mockType);
6073
} // no need to check for GenericArrayType, as Mockito cannot mock this anyway
6174

6275
return result;
6376
}
6477

78+
private Stream<Type> getSuperTypes(Class<?> concreteMockClass) {
79+
Stream<Type> mockInterfaces = Arrays.stream(concreteMockClass.getGenericInterfaces());
80+
Stream<Type> mockSuperTypes =
81+
Stream.concat(mockInterfaces, Stream.of(concreteMockClass.getGenericSuperclass()));
82+
return mockSuperTypes;
83+
}
84+
6585
private boolean recurseOnTypeArguments(
6686
Field injectMocksField, Type[] actualTypeArguments, Type[] actualTypeArguments2) {
6787
boolean isCompatible = true;
@@ -76,30 +96,44 @@ private boolean recurseOnTypeArguments(
7696
// The TypeVariable`s actual type is declared by the field containing
7797
// the object under test, i.e. the field annotated with @InjectMocks
7898
// e.g. @InjectMocks ClassUnderTest<String, Integer> underTest = ..
79-
Type[] injectMocksFieldTypeParameters =
80-
((ParameterizedType) injectMocksField.getGenericType())
81-
.getActualTypeArguments();
82-
// Find index of given TypeVariable where it was defined, e.g. 0 for T1 in
83-
// ClassUnderTest<T1, T2>
84-
// (we're always able to find it, otherwise test class wouldn't have compiled))
85-
TypeVariable<?>[] genericTypeParameters =
86-
injectMocksField.getType().getTypeParameters();
87-
int variableIndex = -1;
88-
for (int i2 = 0; i2 < genericTypeParameters.length; i2++) {
89-
if (genericTypeParameters[i2].equals(typeVariable)) {
90-
variableIndex = i2;
91-
break;
99+
100+
Type genericType = injectMocksField.getGenericType();
101+
if (genericType instanceof ParameterizedType) {
102+
Type[] injectMocksFieldTypeParameters =
103+
((ParameterizedType) genericType).getActualTypeArguments();
104+
// Find index of given TypeVariable where it was defined, e.g. 0 for T1 in
105+
// ClassUnderTest<T1, T2>
106+
// (we're always able to find it, otherwise test class wouldn't have compiled))
107+
TypeVariable<?>[] genericTypeParameters =
108+
injectMocksField.getType().getTypeParameters();
109+
int variableIndex = -1;
110+
for (int i2 = 0; i2 < genericTypeParameters.length; i2++) {
111+
if (genericTypeParameters[i2].equals(typeVariable)) {
112+
variableIndex = i2;
113+
break;
114+
}
92115
}
116+
// now test whether actual type for the type variable is compatible, e.g. for
117+
// class ClassUnderTest<T1, T2> {..}
118+
// T1 would be the String in
119+
// ClassUnderTest<String, Integer> underTest = ..
120+
isCompatible &=
121+
isCompatibleTypes(
122+
injectMocksFieldTypeParameters[variableIndex],
123+
actualTypeArgument2,
124+
injectMocksField);
125+
} else {
126+
// must be a concrete class, recurse on super types that may have type
127+
// parameters
128+
isCompatible &=
129+
getSuperTypes((Class<?>) genericType)
130+
.anyMatch(
131+
superType ->
132+
isCompatibleTypes(
133+
superType,
134+
actualTypeArgument2,
135+
injectMocksField));
93136
}
94-
// now test whether actual type for the type variable is compatible, e.g. for
95-
// class ClassUnderTest<T1, T2> {..}
96-
// T1 would be the String in
97-
// ClassUnderTest<String, Integer> underTest = ..
98-
isCompatible &=
99-
isCompatibleTypes(
100-
injectMocksFieldTypeParameters[variableIndex],
101-
actualTypeArgument2,
102-
injectMocksField);
103137
} else {
104138
isCompatible &=
105139
isCompatibleTypes(
@@ -119,12 +153,12 @@ public OngoingInjector filterCandidate(
119153
List<Object> mockTypeMatches = new ArrayList<>();
120154
for (Object mock : mocks) {
121155
if (candidateFieldToBeInjected.getType().isAssignableFrom(mock.getClass())) {
122-
Type genericMockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
123-
Type genericType = candidateFieldToBeInjected.getGenericType();
124-
boolean bothHaveGenericTypeInfo = genericType != null && genericMockType != null;
125-
if (bothHaveGenericTypeInfo) {
156+
Type mockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
157+
Type typeToMock = candidateFieldToBeInjected.getGenericType();
158+
boolean bothHaveTypeInfo = typeToMock != null && mockType != null;
159+
if (bothHaveTypeInfo) {
126160
// be more specific if generic type information is available
127-
if (isCompatibleTypes(genericType, genericMockType, injectMocksField)) {
161+
if (isCompatibleTypes(typeToMock, mockType, injectMocksField)) {
128162
mockTypeMatches.add(mock);
129163
} // else filter out mock, as generic types don't match
130164
} else {

subprojects/junit-jupiter/src/test/java/org/mockitousage/GenericTypeMockTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import static org.junit.jupiter.api.Assertions.assertEquals;
99
import static org.junit.jupiter.api.Assertions.assertNotNull;
1010
import static org.junit.jupiter.api.Assertions.assertNull;
11+
import static org.mockito.MockitoAnnotations.*;
1112

1213
import java.sql.Time;
14+
import java.util.ArrayList;
1315
import java.util.Collection;
1416
import java.util.Date;
1517
import java.util.HashSet;
@@ -18,6 +20,7 @@
1820
import java.util.Set;
1921
import java.util.TreeSet;
2022

23+
import org.junit.jupiter.api.BeforeEach;
2124
import org.junit.jupiter.api.Nested;
2225
import org.junit.jupiter.api.Test;
2326
import org.junit.jupiter.api.extension.ExtendWith;
@@ -260,4 +263,87 @@ void testWithTypeParameters() {
260263
}
261264
}
262265

266+
@Nested
267+
public class InjectConcreteClassInFieldWithTypeParameter {
268+
public class UnderTestWithTypeParameter<T> {
269+
List<T> tList;
270+
}
271+
272+
public class ConcreteStringList extends ArrayList<String> {}
273+
274+
@Mock
275+
ConcreteStringList concreteStringList;
276+
277+
@InjectMocks
278+
UnderTestWithTypeParameter<String> underTestWithTypeParameters = new UnderTestWithTypeParameter<String>();
279+
280+
@Test
281+
void testWithTypeParameters() {
282+
assertNotNull(concreteStringList);
283+
284+
// verify that we can match the type parameters of the class under test
285+
assertEquals(concreteStringList, underTestWithTypeParameters.tList);
286+
}
287+
}
288+
289+
@Nested
290+
public class NoneMatchInjectConcreteClassInFieldWithTypeParameterTest {
291+
public class UnderTestWithTypeParameter<T> {
292+
List<T> tList;
293+
}
294+
295+
public class ConcreteStringList extends ArrayList<String> {}
296+
297+
@Mock
298+
ConcreteStringList concreteStringList;
299+
300+
@InjectMocks
301+
UnderTestWithTypeParameter<Integer> underTestWithTypeParameters = new UnderTestWithTypeParameter<Integer>();
302+
303+
@Test
304+
void testWithTypeParameters() {
305+
assertNotNull(concreteStringList);
306+
307+
// verify that when no concrete type candidate matches, none is injected
308+
assertNull(underTestWithTypeParameters.tList);
309+
}
310+
}
311+
312+
/**
313+
* Verify regression https://github.com/mockito/mockito/issues/2958 is fixed.
314+
*/
315+
@Nested
316+
public class RegressionClassCastException {
317+
public class AbstractUnderTest<A extends AbstractUnderTest<A>> {
318+
UnderTestInstance<A> instance;
319+
}
320+
321+
public class UnderTestInstance<I extends AbstractUnderTest<I>> {
322+
}
323+
324+
public class ConcreteUnderTest extends AbstractUnderTest<ConcreteUnderTest> {
325+
}
326+
327+
@Mock
328+
UnderTestInstance<ConcreteUnderTest> instanceMock;
329+
330+
@InjectMocks
331+
protected ConcreteUnderTest concreteUnderTest = new ConcreteUnderTest();
332+
333+
@BeforeEach
334+
public void initMocks()
335+
{
336+
openMocks(this);
337+
}
338+
339+
@Test
340+
public void testMockExists() {
341+
assertNotNull(instanceMock);
342+
assertEquals(instanceMock, concreteUnderTest.instance);
343+
}
344+
345+
346+
}
347+
263348
}
349+

0 commit comments

Comments
 (0)