Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ClassCastException #2962

Merged
merged 2 commits into from Apr 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}


}

}