diff --git a/src/main/java/org/mockito/MockSettings.java b/src/main/java/org/mockito/MockSettings.java index 6958bcb347..e9c75c3a1e 100644 --- a/src/main/java/org/mockito/MockSettings.java +++ b/src/main/java/org/mockito/MockSettings.java @@ -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; @@ -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); } diff --git a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java index 95356f5c9a..fed851dbe7 100644 --- a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java +++ b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java @@ -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()); diff --git a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java index fa9af798f2..cd51942581 100644 --- a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java @@ -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())); @@ -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()); diff --git a/src/main/java/org/mockito/internal/configuration/injection/PropertyAndSetterInjection.java b/src/main/java/org/mockito/internal/configuration/injection/PropertyAndSetterInjection.java index 7f267d2c5e..ed32c8eea4 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/PropertyAndSetterInjection.java +++ b/src/main/java/org/mockito/internal/configuration/injection/PropertyAndSetterInjection.java @@ -81,6 +81,7 @@ public boolean processInjection( injectMockCandidates( fieldClass, fieldInstanceNeedingInjection, + injectMocksField, newMockSafeHashSet(mockCandidates)); fieldClass = fieldClass.getSuperclass(); } @@ -100,24 +101,32 @@ private FieldInitializationReport initializeInjectMocksField(Field field, Object } private boolean injectMockCandidates( - Class awaitingInjectionClazz, Object injectee, Set mocks) { + Class awaitingInjectionClazz, + Object injectee, + Field injectMocksField, + Set mocks) { boolean injectionOccurred; List 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 mocks, Object injectee, + Field injectMocksField, boolean injectionOccurred, List orderedCandidateInjecteeFields) { for (Iterator it = orderedCandidateInjecteeFields.iterator(); it.hasNext(); ) { @@ -125,7 +134,11 @@ private boolean injectMockCandidatesOnFields( Object injected = mockCandidateFilter .filterCandidate( - mocks, candidateField, orderedCandidateInjecteeFields, injectee) + mocks, + candidateField, + orderedCandidateInjecteeFields, + injectee, + injectMocksField) .thenInject(); if (injected != null) { injectionOccurred |= true; diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/MockCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/MockCandidateFilter.java index 470a42ff17..82c913695e 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/MockCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/MockCandidateFilter.java @@ -13,5 +13,6 @@ OngoingInjector filterCandidate( Collection mocks, Field candidateFieldToBeInjected, List allRemainingCandidateFields, - Object injectee); + Object injectee, + Field injectMocksField); } diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/NameBasedCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/NameBasedCandidateFilter.java index 125b9595ef..b2cc6c75d5 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/NameBasedCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/NameBasedCandidateFilter.java @@ -23,7 +23,8 @@ public OngoingInjector filterCandidate( final Collection mocks, final Field candidateFieldToBeInjected, final List allRemainingCandidateFields, - final Object injectee) { + final Object injectee, + final Field injectMocksField) { if (mocks.size() == 1 && anotherCandidateMatchesMockName( mocks, candidateFieldToBeInjected, allRemainingCandidateFields)) { @@ -34,7 +35,8 @@ && anotherCandidateMatchesMockName( tooMany(mocks) ? selectMatchingName(mocks, candidateFieldToBeInjected) : mocks, candidateFieldToBeInjected, allRemainingCandidateFields, - injectee); + injectee, + injectMocksField); } private boolean tooMany(Collection mocks) { diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java index ec9dadcfa6..5726ee2015 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java @@ -28,7 +28,8 @@ public OngoingInjector filterCandidate( final Collection mocks, final Field candidateFieldToBeInjected, final List allRemainingCandidateFields, - final Object injectee) { + final Object injectee, + final Field injectMocksField) { if (mocks.size() == 1) { final Object matchingMock = mocks.iterator().next(); diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/TypeBasedCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/TypeBasedCandidateFilter.java index 9ba0fba986..0c40478daa 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/TypeBasedCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/TypeBasedCandidateFilter.java @@ -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; @@ -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 { List tList; Set 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 underTest = .. + Type[] injectMocksFieldTypeParameters = + ((ParameterizedType) injectMocksField.getGenericType()) + .getActualTypeArguments(); + // Find index of given TypeVariable where it was defined, e.g. 0 for T1 in + // ClassUnderTest + // (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 would be the String in + // ClassUnderTest underTest = .. + isCompatible &= + isCompatibleTypes( + injectMocksFieldTypeParameters[variableIndex], + actualTypeArgument2, + injectMocksField); + } else { + isCompatible &= + isCompatibleTypes( + actualTypeArgument, actualTypeArgument2, injectMocksField); + } + } + return isCompatible; + } + @Override public OngoingInjector filterCandidate( final Collection mocks, final Field candidateFieldToBeInjected, final List allRemainingCandidateFields, - final Object injectee) { + final Object injectee, + final Field injectMocksField) { List 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); } } diff --git a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java index a25299722c..7bef7764d4 100644 --- a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java +++ b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java @@ -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; @@ -260,6 +261,12 @@ public MockSettings mockMaker(String mockMaker) { return this; } + @Override + public MockSettings genericTypeToMock(Type genericType) { + this.genericTypeToMock = genericType; + return this; + } + private static CreationSettings validatedSettings( Class typeToMock, CreationSettings source) { MockCreationValidator validator = new MockCreationValidator(); diff --git a/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java b/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java index 4b50781833..9114bcb8cf 100644 --- a/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java +++ b/src/main/java/org/mockito/internal/creation/settings/CreationSettings.java @@ -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; @@ -25,6 +26,7 @@ public class CreationSettings implements MockCreationSettings, Serializabl private static final long serialVersionUID = -6789800638070123629L; protected Class typeToMock; + protected Type genericTypeToMock; protected Set> extraInterfaces = new LinkedHashSet<>(); protected String name; protected Object spiedInstance; @@ -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; @@ -82,6 +85,11 @@ public CreationSettings setTypeToMock(Class typeToMock) { return this; } + public CreationSettings setGenericTypeToMock(Type genericTypeToMock) { + this.genericTypeToMock = genericTypeToMock; + return this; + } + @Override public Set> getExtraInterfaces() { return extraInterfaces; @@ -185,4 +193,9 @@ public Strictness getStrictness() { public String getMockMaker() { return mockMaker; } + + @Override + public Type getGenericTypeToMock() { + return genericTypeToMock; + } } diff --git a/src/main/java/org/mockito/mock/MockCreationSettings.java b/src/main/java/org/mockito/mock/MockCreationSettings.java index 94c74558ee..949af03b2e 100644 --- a/src/main/java/org/mockito/mock/MockCreationSettings.java +++ b/src/main/java/org/mockito/mock/MockCreationSettings.java @@ -4,6 +4,7 @@ */ package org.mockito.mock; +import java.lang.reflect.Type; import java.util.List; import java.util.Set; @@ -27,6 +28,11 @@ public interface MockCreationSettings { */ Class getTypeToMock(); + /** + * The generic type of the mock, if any. + */ + Type getGenericTypeToMock(); + /** * the extra interfaces the mock object should implement. */ diff --git a/subprojects/junit-jupiter/src/test/java/org/mockitousage/GenericTypeMockTest.java b/subprojects/junit-jupiter/src/test/java/org/mockitousage/GenericTypeMockTest.java new file mode 100644 index 0000000000..cfece7cb84 --- /dev/null +++ b/subprojects/junit-jupiter/src/test/java/org/mockitousage/GenericTypeMockTest.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2023 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +package org.mockitousage; + +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 java.sql.Time; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests that verify Mockito can discern mocks by generic types, so if there are multiple mock candidates + * with the same generic type but different type parameters available for injection into a given field, + * Mockito won't fail to inject (even if mock field name doesn't match under test's field name). + */ +@ExtendWith(MockitoExtension.class) +public class GenericTypeMockTest { + + + @Nested + public class SingleTypeParamTest { + public class UnderTestWithSingleTypeParam { + List stringList; + List intList; + } + + @Mock + private List stringListMock; + + @Mock + private List intListMock; + + // must construct non-static inner class ourselves here + // (making it public static classes doesn't work either) + @InjectMocks + private UnderTestWithSingleTypeParam underTestWithSingleTypeParam = new UnderTestWithSingleTypeParam(); + + @Test + void testSingleTypeParam() { + // testing for not null first before testing for equals, + // because assertEquals(null, null) == true, + // so test would succeed if both @Mock and @InjectMocks + // don't work at all + assertNotNull(stringListMock); + assertNotNull(intListMock); + + assertEquals(stringListMock, underTestWithSingleTypeParam.stringList); + assertEquals(intListMock, underTestWithSingleTypeParam.intList); + } + } + + @Nested + public class WildcardTest { + class UnderTestWithWildcard { + Set dateSet; + Set numberSet; + } + + @Mock + Set