How to capture a list of specific type with mockito

后端 未结 8 1585
天命终不由人
天命终不由人 2020-12-12 10:05

Is there a way to capture a list of specific type using mockitos ArgumentCaptore. This doesn\'t work:

ArgumentCaptor> argumen         


        
相关标签:
8条回答
  • 2020-12-12 10:45

    The nested generics-problem can be avoided with the @Captor annotation:

    public class Test{
    
        @Mock
        private Service service;
    
        @Captor
        private ArgumentCaptor<ArrayList<SomeType>> captor;
    
        @Before
        public void init(){
            MockitoAnnotations.initMocks(this);
        }
    
        @Test 
        public void shouldDoStuffWithListValues() {
            //...
            verify(service).doStuff(captor.capture()));
        }
    }
    
    0 讨论(0)
  • 2020-12-12 10:46
    List<String> mockedList = mock(List.class);
    
    List<String> l = new ArrayList();
    l.add("someElement");
    
    mockedList.addAll(l);
    
    ArgumentCaptor<List> argumentCaptor = ArgumentCaptor.forClass(List.class);
    
    verify(mockedList).addAll(argumentCaptor.capture());
    
    List<String> capturedArgument = argumentCaptor.<List<String>>getValue();
    
    assertThat(capturedArgument, hasItem("someElement"));
    
    0 讨论(0)
  • 2020-12-12 10:52

    I had the same issue with testing activity in my Android app. I used ActivityInstrumentationTestCase2 and MockitoAnnotations.initMocks(this); didn't work. I solved this issue with another class with respectively field. For example:

    class CaptorHolder {
    
            @Captor
            ArgumentCaptor<Callback<AuthResponse>> captor;
    
            public CaptorHolder() {
                MockitoAnnotations.initMocks(this);
            }
        }
    

    Then, in activity test method:

    HubstaffService hubstaffService = mock(HubstaffService.class);
    fragment.setHubstaffService(hubstaffService);
    
    CaptorHolder captorHolder = new CaptorHolder();
    ArgumentCaptor<Callback<AuthResponse>> captor = captorHolder.captor;
    
    onView(withId(R.id.signInBtn))
            .perform(click());
    
    verify(hubstaffService).authorize(anyString(), anyString(), captor.capture());
    Callback<AuthResponse> callback = captor.getValue();
    
    0 讨论(0)
  • 2020-12-12 10:55

    Yeah, this is a general generics problem, not mockito-specific.

    There is no class object for ArrayList<SomeType>, and thus you can't type-safely pass such an object to a method requiring a Class<ArrayList<SomeType>>.

    You can cast the object to the right type:

    Class<ArrayList<SomeType>> listClass =
                  (Class<ArrayList<SomeType>>)(Class)ArrayList.class;
    ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(listClass);
    

    This will give some warnings about unsafe casts, and of course your ArgumentCaptor can't really differentiate between ArrayList<SomeType> and ArrayList<AnotherType> without maybe inspecting the elements.

    (As mentioned in the other answer, while this is a general generics problem, there is a Mockito-specific solution for the type-safety problem with the @Captor annotation. It still can't distinguish between an ArrayList<SomeType> and an ArrayList<OtherType>.)

    Edit:

    Take also a look at tenshis comment. You can change the original code from Paŭlo Ebermann to this (much simpler)

    final ArgumentCaptor<List<SomeType>> listCaptor
            = ArgumentCaptor.forClass((Class) List.class);
    
    0 讨论(0)
  • 2020-12-12 10:56

    There is an open issue in Mockito's GitHub about this exact problem.

    I have found a simple workaround that does not force you to use annotations in your tests:

    import org.mockito.ArgumentCaptor;
    import org.mockito.Captor;
    import org.mockito.MockitoAnnotations;
    
    public final class MockitoCaptorExtensions {
    
        public static <T> ArgumentCaptor<T> captorFor(final CaptorTypeReference<T> argumentTypeReference) {
            return new CaptorContainer<T>().captor;
        }
    
        public static <T> ArgumentCaptor<T> captorFor(final Class<T> argumentClass) {
            return ArgumentCaptor.forClass(argumentClass);
        }
    
        public interface CaptorTypeReference<T> {
    
            static <T> CaptorTypeReference<T> genericType() {
                return new CaptorTypeReference<T>() {
                };
            }
    
            default T nullOfGenericType() {
                return null;
            }
    
        }
    
        private static final class CaptorContainer<T> {
    
            @Captor
            private ArgumentCaptor<T> captor;
    
            private CaptorContainer() {
                MockitoAnnotations.initMocks(this);
            }
    
        }
    
    }
    

    What happens here is that we create a new class with the @Captor annotation and inject the captor into it. Then we just extract the captor and return it from our static method.

    In your test you can use it like so:

    ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(genericType());
    

    Or with syntax that resembles Jackson's TypeReference:

    ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(
        new CaptorTypeReference<Supplier<Set<List<Object>>>>() {
        }
    );
    

    It works, because Mockito doesn't actually need any type information (unlike serializers, for example).

    0 讨论(0)
  • 2020-12-12 11:00

    For an earlier version of junit, you can do

    Class<Map<String, String>> mapClass = (Class) Map.class;
    ArgumentCaptor<Map<String, String>> mapCaptor = ArgumentCaptor.forClass(mapClass);
    
    0 讨论(0)
提交回复
热议问题