Skip to content

Commit 393f0a1

Browse files
jfrantziusJörg von Frantzius
andauthored
Fail when InjectMocks has multiple candidate mocks to insert into field (#2942)
Fixes #2934 Co-authored-by: Jörg von Frantzius <joerg.frantzius@aperto.com>
1 parent b6861e3 commit 393f0a1

File tree

4 files changed

+115
-32
lines changed

4 files changed

+115
-32
lines changed

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
package org.mockito.internal.configuration.injection.filter;
66

7+
import static org.mockito.internal.exceptions.Reporter.moreThanOneMockCandidate;
8+
79
import java.lang.reflect.Field;
810
import java.lang.reflect.ParameterizedType;
911
import java.lang.reflect.Type;
@@ -136,11 +138,24 @@ public OngoingInjector filterCandidate(
136138
// being wrapped), and MockUtil.getMockSettings() throws exception for those
137139
}
138140

139-
return next.filterCandidate(
140-
mockTypeMatches,
141-
candidateFieldToBeInjected,
142-
allRemainingCandidateFields,
143-
injectee,
144-
injectMocksField);
141+
boolean wasMultipleMatches = mockTypeMatches.size() > 1;
142+
143+
OngoingInjector result =
144+
next.filterCandidate(
145+
mockTypeMatches,
146+
candidateFieldToBeInjected,
147+
allRemainingCandidateFields,
148+
injectee,
149+
injectMocksField);
150+
151+
if (wasMultipleMatches) {
152+
// we had found multiple mocks matching by type, see whether following filters
153+
// were able to reduce this to single match (e.g. by filtering for matching field names)
154+
if (result == OngoingInjector.nop) {
155+
// nope, following filters cannot reduce this to a single match
156+
throw moreThanOneMockCandidate(candidateFieldToBeInjected, mocks);
157+
}
158+
}
159+
return result;
145160
}
146161
}

src/main/java/org/mockito/internal/exceptions/Reporter.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Arrays;
1515
import java.util.Collection;
1616
import java.util.List;
17+
import java.util.stream.Collectors;
1718

1819
import org.mockito.exceptions.base.MockitoAssertionError;
1920
import org.mockito.exceptions.base.MockitoException;
@@ -53,6 +54,7 @@
5354
import org.mockito.invocation.Location;
5455
import org.mockito.invocation.MatchableInvocation;
5556
import org.mockito.listeners.InvocationListener;
57+
import org.mockito.mock.MockName;
5658
import org.mockito.mock.SerializableMode;
5759

5860
/**
@@ -893,6 +895,29 @@ public static MockitoException cannotInjectDependency(
893895
details);
894896
}
895897

898+
public static MockitoException moreThanOneMockCandidate(
899+
Field field, Collection<?> mockCandidates) {
900+
List<String> mockNames =
901+
mockCandidates.stream()
902+
.map(MockUtil::getMockName)
903+
.map(MockName::toString)
904+
.collect(Collectors.toList());
905+
return new MockitoException(
906+
join(
907+
"Mockito couldn't inject mock dependency on field "
908+
+ "'"
909+
+ field
910+
+ "' that is annotated with @InjectMocks in your test, ",
911+
"because there were multiple matching mocks (i.e. "
912+
+ "fields annotated with @Mock and having matching type): "
913+
+ String.join(", ", mockNames)
914+
+ ".",
915+
"If you have multiple fields of same type in your class under test "
916+
+ "then consider naming the @Mock fields "
917+
+ "identically to the respective class under test's fields, "
918+
+ "so Mockito can match them by name."));
919+
}
920+
896921
private static String exceptionCauseMessageIfAvailable(Exception details) {
897922
if (details.getCause() == null) {
898923
return details.getMessage();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2023 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
6+
package org.mockitousage;
7+
8+
import static org.junit.jupiter.api.Assertions.assertThrows;
9+
10+
import java.util.List;
11+
12+
import org.junit.jupiter.api.Nested;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.BeforeEachCallback;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
import org.junit.jupiter.api.extension.ExtensionContext;
17+
import org.mockito.InjectMocks;
18+
import org.mockito.Mock;
19+
import org.mockito.exceptions.base.MockitoException;
20+
import org.mockito.junit.jupiter.MockitoExtension;
21+
22+
/**
23+
* Verify that a {@link MockitoException} is thrown when there are multiple {@link Mock} fields that
24+
* do match a candidate field by type, but cannot be matched by name.
25+
*
26+
* Uses a JUnit 5 extension to obtain the JUnit 5 {@link ExtensionContext} and
27+
* pass it to {@link MockitoExtension#beforeEach(ExtensionContext)}, as the exception
28+
* is thrown during {@link org.junit.jupiter.api.BeforeEach}.
29+
*/
30+
@ExtendWith(GenericTypeMockMultipleMatchesTest.ContextProvidingExtension.class)
31+
public class GenericTypeMockMultipleMatchesTest {
32+
33+
private static ExtensionContext currentExtensionContext;
34+
35+
public static class ContextProvidingExtension implements BeforeEachCallback {
36+
@Override
37+
public void beforeEach(ExtensionContext context) throws Exception {
38+
currentExtensionContext = context;
39+
}
40+
}
41+
42+
private void startMocking(Object testInstance) {
43+
MockitoExtension mockitoExtension = new MockitoExtension();
44+
mockitoExtension.beforeEach(currentExtensionContext);
45+
}
46+
47+
@Nested
48+
public class MultipleCandidatesByTypeTest {
49+
public class UnderTestWithMultipleCandidatesByType {
50+
List<String> stringList;
51+
}
52+
53+
@Mock
54+
List<String> stringList1;
55+
56+
@Mock
57+
List<String> stringList2;
58+
59+
@InjectMocks
60+
UnderTestWithMultipleCandidatesByType underTestWithMultipleCandidates = new UnderTestWithMultipleCandidatesByType();
61+
62+
@Test
63+
void testMultipleCandidatesByTypes() {
64+
assertThrows(MockitoException.class, () -> startMocking(this));
65+
}
66+
}
67+
68+
69+
}

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

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
@ExtendWith(MockitoExtension.class)
3434
public class GenericTypeMockTest {
3535

36-
3736
@Nested
3837
public class SingleTypeParamTest {
3938
public class UnderTestWithSingleTypeParam {
@@ -147,31 +146,6 @@ void testGenericSubclass() {
147146
}
148147
}
149148

150-
@Nested
151-
public class MultipleCandidatesByTypeTest {
152-
public class UnderTestWithMultipleCandidatesByType {
153-
List<String> stringList;
154-
}
155-
156-
@Mock
157-
List<String> stringList1;
158-
159-
@Mock
160-
List<String> stringList2;
161-
162-
@InjectMocks
163-
UnderTestWithMultipleCandidatesByType underTestWithMultipleCandidates = new UnderTestWithMultipleCandidatesByType();
164-
165-
@Test
166-
void testMultipleCandidatesByTypes() {
167-
assertNotNull(stringList1);
168-
assertNotNull(stringList2);
169-
170-
// verify that when mutiple mock candidates exist with same type (but not matching by field names), none will be injected
171-
assertNull(underTestWithMultipleCandidates.stringList);
172-
}
173-
}
174-
175149
@Nested
176150
public class MultipleCandidatesOneByNameTest {
177151
public class UnderTestWithMultipleCandidatesOneByName {

0 commit comments

Comments
 (0)