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

#2934 throw exception on multiple matches by type if cannot be reduce… #2942

Merged
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 @@ -4,6 +4,8 @@
*/
package org.mockito.internal.configuration.injection.filter;

import static org.mockito.internal.exceptions.Reporter.moreThanOneMockCandidate;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
Expand Down Expand Up @@ -136,11 +138,24 @@ public OngoingInjector filterCandidate(
// being wrapped), and MockUtil.getMockSettings() throws exception for those
}

return next.filterCandidate(
mockTypeMatches,
candidateFieldToBeInjected,
allRemainingCandidateFields,
injectee,
injectMocksField);
boolean wasMultipleMatches = mockTypeMatches.size() > 1;

OngoingInjector result =
next.filterCandidate(
mockTypeMatches,
candidateFieldToBeInjected,
allRemainingCandidateFields,
injectee,
injectMocksField);

if (wasMultipleMatches) {
// we had found multiple mocks matching by type, see whether following filters
// were able to reduce this to single match (e.g. by filtering for matching field names)
if (result == OngoingInjector.nop) {
// nope, following filters cannot reduce this to a single match
throw moreThanOneMockCandidate(candidateFieldToBeInjected, mocks);
}
}
return result;
}
}
25 changes: 25 additions & 0 deletions src/main/java/org/mockito/internal/exceptions/Reporter.java
Expand Up @@ -14,6 +14,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import org.mockito.exceptions.base.MockitoAssertionError;
import org.mockito.exceptions.base.MockitoException;
Expand Down Expand Up @@ -53,6 +54,7 @@
import org.mockito.invocation.Location;
import org.mockito.invocation.MatchableInvocation;
import org.mockito.listeners.InvocationListener;
import org.mockito.mock.MockName;
import org.mockito.mock.SerializableMode;

/**
Expand Down Expand Up @@ -893,6 +895,29 @@ public static MockitoException cannotInjectDependency(
details);
}

public static MockitoException moreThanOneMockCandidate(
Field field, Collection<?> mockCandidates) {
List<String> mockNames =
mockCandidates.stream()
.map(MockUtil::getMockName)
.map(MockName::toString)
.collect(Collectors.toList());
return new MockitoException(
join(
"Mockito couldn't inject mock dependency on field "
+ "'"
+ field
+ "' that is annotated with @InjectMocks in your test, ",
"because there were multiple matching mocks (i.e. "
+ "fields annotated with @Mock and having matching type): "
+ String.join(", ", mockNames)
+ ".",
"If you have multiple fields of same type in your class under test "
+ "then consider naming the @Mock fields "
+ "identically to the respective class under test's fields, "
+ "so Mockito can match them by name."));
}

private static String exceptionCauseMessageIfAvailable(Exception details) {
if (details.getCause() == null) {
return details.getMessage();
Expand Down
@@ -0,0 +1,69 @@
/*
* 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.assertThrows;

import java.util.List;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.junit.jupiter.MockitoExtension;

/**
* Verify that a {@link MockitoException} is thrown when there are multiple {@link Mock} fields that
* do match a candidate field by type, but cannot be matched by name.
*
* Uses a JUnit 5 extension to obtain the JUnit 5 {@link ExtensionContext} and
* pass it to {@link MockitoExtension#beforeEach(ExtensionContext)}, as the exception
* is thrown during {@link org.junit.jupiter.api.BeforeEach}.
*/
@ExtendWith(GenericTypeMockMultipleMatchesTest.ContextProvidingExtension.class)
public class GenericTypeMockMultipleMatchesTest {

private static ExtensionContext currentExtensionContext;

public static class ContextProvidingExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
currentExtensionContext = context;
}
}

private void startMocking(Object testInstance) {
MockitoExtension mockitoExtension = new MockitoExtension();
mockitoExtension.beforeEach(currentExtensionContext);
}

@Nested
public class MultipleCandidatesByTypeTest {
public class UnderTestWithMultipleCandidatesByType {
List<String> stringList;
}

@Mock
List<String> stringList1;

@Mock
List<String> stringList2;

@InjectMocks
UnderTestWithMultipleCandidatesByType underTestWithMultipleCandidates = new UnderTestWithMultipleCandidatesByType();

@Test
void testMultipleCandidatesByTypes() {
assertThrows(MockitoException.class, () -> startMocking(this));
}
}


}
Expand Up @@ -33,7 +33,6 @@
@ExtendWith(MockitoExtension.class)
public class GenericTypeMockTest {


@Nested
public class SingleTypeParamTest {
public class UnderTestWithSingleTypeParam {
Expand Down Expand Up @@ -147,31 +146,6 @@ void testGenericSubclass() {
}
}

@Nested
public class MultipleCandidatesByTypeTest {
public class UnderTestWithMultipleCandidatesByType {
List<String> stringList;
}

@Mock
List<String> stringList1;

@Mock
List<String> stringList2;

@InjectMocks
UnderTestWithMultipleCandidatesByType underTestWithMultipleCandidates = new UnderTestWithMultipleCandidatesByType();

@Test
void testMultipleCandidatesByTypes() {
assertNotNull(stringList1);
assertNotNull(stringList2);

// verify that when mutiple mock candidates exist with same type (but not matching by field names), none will be injected
assertNull(underTestWithMultipleCandidates.stringList);
}
}

@Nested
public class MultipleCandidatesOneByNameTest {
public class UnderTestWithMultipleCandidatesOneByName {
Expand Down