Skip to content

Commit

Permalink
Fix ClassCastException regression with parameterized types with Injec…
Browse files Browse the repository at this point in the history
…tMocks (#2962)

Fixes #2958

Co-authored-by: Jörg von Frantzius <joerg.frantzius@aperto.com>
  • Loading branch information
jfrantzius and Jörg von Frantzius committed Apr 9, 2023
1 parent d65bba7 commit 84a5eee
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 46 deletions.
Expand Up @@ -15,6 +15,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

import org.mockito.internal.util.MockUtil;

Expand All @@ -28,26 +29,38 @@ public TypeBasedCandidateFilter(MockCandidateFilter 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;
if (typeToMock instanceof ParameterizedType) {
if (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 {
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
// mockType is a non-parameterized Class, i.e. a concrete class.
// so walk concrete class' type hierarchy
Class<?> concreteMockClass = (Class<?>) mockType;
Stream<Type> mockSuperTypes = getSuperTypes(concreteMockClass);
result =
recurseOnTypeArguments(
injectMocksField, actualTypeArguments, actualTypeArguments2);
mockSuperTypes.anyMatch(
mockSuperType ->
isCompatibleTypes(
typeToMock, mockSuperType, injectMocksField));
}
} else if (typeToMock instanceof WildcardType) {
WildcardType wildcardTypeToMock = (WildcardType) typeToMock;
Expand All @@ -56,12 +69,19 @@ protected boolean isCompatibleTypes(Type typeToMock, Type mockType, Field inject
Arrays.stream(upperBounds)
.anyMatch(t -> isCompatibleTypes(t, mockType, injectMocksField));
} else if (typeToMock instanceof Class && mockType instanceof Class) {
result = ((Class) typeToMock).isAssignableFrom((Class) mockType);
result = ((Class<?>) typeToMock).isAssignableFrom((Class<?>) mockType);
} // no need to check for GenericArrayType, as Mockito cannot mock this anyway

return result;
}

private Stream<Type> getSuperTypes(Class<?> concreteMockClass) {
Stream<Type> mockInterfaces = Arrays.stream(concreteMockClass.getGenericInterfaces());
Stream<Type> mockSuperTypes =
Stream.concat(mockInterfaces, Stream.of(concreteMockClass.getGenericSuperclass()));
return mockSuperTypes;
}

private boolean recurseOnTypeArguments(
Field injectMocksField, Type[] actualTypeArguments, Type[] actualTypeArguments2) {
boolean isCompatible = true;
Expand All @@ -76,30 +96,44 @@ private boolean recurseOnTypeArguments(
// 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;

Type genericType = injectMocksField.getGenericType();
if (genericType instanceof ParameterizedType) {
Type[] injectMocksFieldTypeParameters =
((ParameterizedType) genericType).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 {
// must be a concrete class, recurse on super types that may have type
// parameters
isCompatible &=
getSuperTypes((Class<?>) genericType)
.anyMatch(
superType ->
isCompatibleTypes(
superType,
actualTypeArgument2,
injectMocksField));
}
// 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(
Expand All @@ -119,12 +153,12 @@ public OngoingInjector filterCandidate(
List<Object> mockTypeMatches = new ArrayList<>();
for (Object mock : mocks) {
if (candidateFieldToBeInjected.getType().isAssignableFrom(mock.getClass())) {
Type genericMockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
Type genericType = candidateFieldToBeInjected.getGenericType();
boolean bothHaveGenericTypeInfo = genericType != null && genericMockType != null;
if (bothHaveGenericTypeInfo) {
Type mockType = MockUtil.getMockSettings(mock).getGenericTypeToMock();
Type typeToMock = candidateFieldToBeInjected.getGenericType();
boolean bothHaveTypeInfo = typeToMock != null && mockType != null;
if (bothHaveTypeInfo) {
// be more specific if generic type information is available
if (isCompatibleTypes(genericType, genericMockType, injectMocksField)) {
if (isCompatibleTypes(typeToMock, mockType, injectMocksField)) {
mockTypeMatches.add(mock);
} // else filter out mock, as generic types don't match
} else {
Expand Down
Expand Up @@ -8,8 +8,10 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.MockitoAnnotations.*;

import java.sql.Time;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
Expand All @@ -18,6 +20,7 @@
import java.util.Set;
import java.util.TreeSet;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand Down Expand Up @@ -260,4 +263,87 @@ void testWithTypeParameters() {
}
}

@Nested
public class InjectConcreteClassInFieldWithTypeParameter {
public class UnderTestWithTypeParameter<T> {
List<T> tList;
}

public class ConcreteStringList extends ArrayList<String> {}

@Mock
ConcreteStringList concreteStringList;

@InjectMocks
UnderTestWithTypeParameter<String> underTestWithTypeParameters = new UnderTestWithTypeParameter<String>();

@Test
void testWithTypeParameters() {
assertNotNull(concreteStringList);

// verify that we can match the type parameters of the class under test
assertEquals(concreteStringList, underTestWithTypeParameters.tList);
}
}

@Nested
public class NoneMatchInjectConcreteClassInFieldWithTypeParameterTest {
public class UnderTestWithTypeParameter<T> {
List<T> tList;
}

public class ConcreteStringList extends ArrayList<String> {}

@Mock
ConcreteStringList concreteStringList;

@InjectMocks
UnderTestWithTypeParameter<Integer> underTestWithTypeParameters = new UnderTestWithTypeParameter<Integer>();

@Test
void testWithTypeParameters() {
assertNotNull(concreteStringList);

// verify that when no concrete type candidate matches, none is injected
assertNull(underTestWithTypeParameters.tList);
}
}

/**
* Verify regression https://github.com/mockito/mockito/issues/2958 is fixed.
*/
@Nested
public class RegressionClassCastException {
public class AbstractUnderTest<A extends AbstractUnderTest<A>> {
UnderTestInstance<A> instance;
}

public class UnderTestInstance<I extends AbstractUnderTest<I>> {
}

public class ConcreteUnderTest extends AbstractUnderTest<ConcreteUnderTest> {
}

@Mock
UnderTestInstance<ConcreteUnderTest> instanceMock;

@InjectMocks
protected ConcreteUnderTest concreteUnderTest = new ConcreteUnderTest();

@BeforeEach
public void initMocks()
{
openMocks(this);
}

@Test
public void testMockExists() {
assertNotNull(instanceMock);
assertEquals(instanceMock, concreteUnderTest.instance);
}


}

}

0 comments on commit 84a5eee

Please sign in to comment.