Search code examples
javajunitmockingmockitovariadic-functions

Mockito Varargs parameter when thenAnswer


I don't understand why a Class Cast exception is thrown here:

I have a method that takes as parameter a varargs of String

public List<A> getSomething(final String a, final String b, final Date c, final String d, final String e, final String... f) throws MyException {

   return valuesFromDB(a, b, c, d, e, f);

}

I have mocked the method implementation

public List<A> getSomethingUnitTest(final String a, final String b, final Date c, final String d, final String e, final String... f) throws MyException {

   return valuesFromCSV(a, b, c, d, e, f);

}

on Unit Test using Mockito

@ExtendWith(MockitoExtension.class)
class AlgoTest {

    @Test
    void testExecute() throws MyException {
    
        when(myRealService.getSomething(anyString(), anyString(), any(Date.class), anyString(), anyString(), any()))
            .thenAnswer(i -> myMockedService.getSomethingUnitTest(i.getArgument(0), i.getArgument(1), i.getArgument(2), i.getArgument(3), i.getArgument(4), i.getArgument(5)));

        Output output = algo.execute();
        
        assertNotNull(output);
   
    }

}

when I run the test a Class Cast Exception is thrown

java.lang.ClassCastException: class java.lang.String cannot be cast to class [Ljava.lang.String; (java.lang.String and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
at com.cmystuff.alghorithm.AlgoTest.lambda$2(AlgoTest.java:82)
at org.mockito.internal.stubbing.StubbedInvocationMatcher.answer(StubbedInvocationMatcher.java:40)
at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:99)
at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:29)
at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:33)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:82)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:56)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor$DispatcherDefaultingToRealMethod.interceptSuperCallable(MockMethodInterceptor.java:141)

any doucumentation:

<String[]> String[] org.mockito.ArgumentMatchers.any()
Matches anything, including nulls and varargs. 

getArgument Documentation:

<String[]> String[] org.mockito.invocation.InvocationOnMock.getArgument(int index)

Returns casted argument at the given index. Can lookup in expanded arguments form getArguments().
This method is preferred over getArgument(int, Class) for readability. 
Please readthe documentation of getArgument(int, Class) for an overview of situations whenthat method is preferred over this one.

I also tried to use anyString() but I got the same Class Cast Exception

@ExtendWith(MockitoExtension.class)
class AlgoTest {

    @Test
    void testExecute() throws MyException {
    
        when(myRealService.getSomething(anyString(), anyString(), any(Date.class), anyString(), anyString(), anyString()))
            .thenAnswer(i -> myMockedService.getSomethingUnitTest(i.getArgument(0), i.getArgument(1), i.getArgument(2), i.getArgument(3), i.getArgument(4), i.getArgument(5)));

        Output output = algo.execute();
        
        assertNotNull(output);
   
    }

}

The issue seems to be related to varargs with one value... if I pass two values for the varargs parameter

@ExtendWith(MockitoExtension.class)
class AlgoTest {

    @Test
    void testExecute() throws MyException {
    
        when(myRealService.getSomething(anyString(), anyString(), any(Date.class), anyString(), anyString(), anyString(), anyString()))
            .thenAnswer(i -> myMockedService.getSomethingUnitTest(i.getArgument(0), i.getArgument(1), i.getArgument(2), i.getArgument(3), i.getArgument(4), i.getArgument(5), i.getArgument(6)));

        Output output = algo.execute();
        
        assertNotNull(output);
   
    }

}

this one will work fine.


Solution

  • Method taking varargs is represented in runtime as a method taking an array as its last argument. The fact that you can pass arguments as separate values in your source code is a syntax sugar.

    The problem with your code is that i.getArgument(index) returns each separate value - it does not group varargs into an array.

    This is combined with incorrect type inference - if you call getSomething with i.getArgument(5), the compiler incorrect assumes that it needs to cast to String[], thus your code is equivalent to:

    when(myRealService.getSomething(
            anyString(),
            anyString(),
            any(Date.class),
            anyString(),
            anyString(),
            any())
    ).thenAnswer(i -> {
        String[] varargs = i.getArgument(5); // incorrect, Note that code works fine if you use String -> the compiler knows that you are using varargs syntax
        return myMockedService.getSomething(
                i.getArgument(0),
                i.getArgument(1),
                i.getArgument(2),
                i.getArgument(3),
                i.getArgument(4),
                varargs
                );
    });
    

    For example, it you call:

    myRealService.getSomething("0", "1", new Date(), "3", "4", "5", "6", "7");
    
    • i.getArgument(5) is equal to "5"
    • you cast this value to an array (BOOM, ClassCastException)
    • you should be passing entire varagrs array instead
    • you can easily copy the values corresponding to original varargs to a new array (of type String[])
    when(myRealService.getSomething(
            anyString(),
            anyString(),
            any(Date.class),
            anyString(),
            anyString(),
            any())
    ).thenAnswer(i -> {
        String[] varargFromInvocation = Arrays.copyOfRange(i.getArguments(), 5, i.getArguments().length, String[].class);
        return myMockedService.getSomething(
                i.getArgument(0),
                i.getArgument(1),
                i.getArgument(2),
                i.getArgument(3),
                i.getArgument(4),
                varargFromInvocation);
    });
    

    Other thoughts: If you only want to stub this one method, maybe you can:

    • extend your real service
    • override getSomething
    • pass that to the algorithm

    Even better:

    • extract SomethingRepository
    • use DBSomethingRepository in prod
    • use CVSSomethingRepository in test