javaspringtestingmockitoresttemplate

Parameteryzed HttpEntity in ArgumentMatcher is not working


I'm making some tests using Mockito and my test are running succesfuly with a wrong parameter in the HttpEntity's generic.

@UnitTest
@RunWith(MockitoJUnitRunner.class)
public class NicoMailingStrategyUnitTest {


    @Mock RestTemplate restTemplate;
    @InjectMocks NicoMailingStrategy nicoMailingStrategy;

    @Test
    public void sendTest() {

        nicoMailingStrategy.send(StubProvider.getEmailMessageModelStub());
    
        Mockito.verify(restTemplate, Mockito.times(1)).exchange(
             Mockito.eq(NicoMailingStrategy.API_ENDPOINT),
             Mockito.eq(HttpMethod.POST),
             ArgumentMatchers.<HttpEntity<FakeSendEmailDTO>>any(), // This DTO is wrong!!
             Mockito.eq(String.class));
    

}

In the 3rd exchange method parameter I'm inserting in the HttpEntity a body of type 'FakeSendEmailDTO' but the real method call uses a 'NicoSendEmailDTO', anyways the test runs well! I'm waiting for it to throw a fail because the real method is not using that DTO.


Solution

  • ArgumentMatchers.any() isn't checking the type of the argument (as in the docs for any()) and will match null too (which doesn't have a type). The type witness is only there to disambiguate between different overloads of the same method (e.g. you are stubbing methods count(Stream<?>) and count(Collection<?>)). The code doesn't even have a way of accessing the actual type parameter at runtime, simply because it is not available at runtime.

    If you need a matcher that checks the type of the argument, then <T> T any(Class<T>) is the way to go. But it doesn't solve your problem, because Class<T> does not have access to nested generic type arguments, only the top-level (raw) types. It can only check if you have passed any object of, e.g. HttpEntity: HttpEntity<A> and HttpEntity<B> is the same type when you ask the JVM (both are HttpEntity.class). You can easily verify that claim yourself (related: What's wrong with Java's generics?):

    final List<String> strings = new ArrayList<String>();
    final List<Integer> ints = new ArrayList<Integer>();
    System.out.println(strings.getClass() == ints.getClass()); // true
    // both are `ArrayList.class`, the generic type parameter is "lost"
    

    Related to that: Java doesn't allow you to overload methods based on the generic type parameters. While you can have count(Stream<?>) and count(Collection<?>), it is not possible to overload a method solely on the generic types. Having sum(List<Integer>) and sum(List<Long>) will not compile, because both methods have the same signature (sum(List)).

    What you can try to do instead is to use the argThat matcher and implement custom logic to check the argument:

    ArgumentMatchers.argThat(entity -> entity.getBody().getClass() == FakeSendEmailDTO.class)
    // or
    ArgumentMatchers.argThat(entity -> entity.getBody() instanceof FakeSendEmailDTO)
    // you might be required to specify the type to make the compiler happy:
    ArgumentMatchers.<HttpEntity<FakeSendEmailDTO>>argThat(entity -> …)
    ArgumentMatchers.argThat((HttpEntity<FakeSendEmailDTO> entity) -> …)
    

    Note that this might already throw when accessing entity.getBody(), due to the implicit cast added for the generic return type. Maybe you can get around this by matching HttpEntity<Object> instead and then checking the dynamic type with .getClass() or instanceof.