Jak przechwycić listę określonego typu za pomocą mockito

301

Czy istnieje sposób na przechwycenie listy określonego typu przy użyciu mockitos ArgumentCaptore. To nie działa:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Andreas Köberle
źródło
8
Uważam, że stosowanie konkretnej implementacji listy tutaj ( ArrayList) jest okropnym pomysłem . Zawsze możesz użyć Listinterfejsu, a jeśli chcesz przedstawić fakt, że jest on kowariantny, możesz użyć extends:ArgumentCaptor<? extends List<SomeType>>
tenshi

Odpowiedzi:

533

Zagnieżdżonego problemu rodzajowego można uniknąć za pomocą adnotacji @Captor :

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()));
    }
}
crunchdog
źródło
70
I wolą używać MockitoAnnotations.initMocks(this)w @Beforesposób niż za pomocą gońca, który wyłącza zdolność do użytkowania innego biegacza. Jednak +1, dziękuję za wskazanie adnotacji.
John B,
4
Nie jestem pewien, czy ten przykład jest kompletny. Dostaję ... Błąd: (240, 40) java: zmienny captor mógł nie zostać zainicjowany lubię odpowiedź tenshi poniżej
Michael Dausmann
1
Natknąłem się na ten sam problem i znalazłem ten post na blogu, który trochę mi pomógł: blog.jdriven.com/2012/10/10 . Zawiera krok do użycia MockitoAnnotations.initMocks po umieszczeniu adnotacji w klasie. Zauważyłem jedną rzecz: nie możesz mieć jej w zmiennej lokalnej.
SlopeOak
1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType>> captor; już przechwytuje tablicę „SomeType” <- to jest określony typ, prawda?
Miguel R. Santaella,
1
Zwykle wolę List zamiast ArrayList w deklaracji Captor: ArgumentCaptor <List <SomeType>> captor;
Miguel R. Santaella,
146

Tak, jest to ogólny problem ogólny, nie związany z mockito.

Nie ma obiektu klasy ArrayList<SomeType>, a zatem nie można bezpiecznie wpisać takiego obiektu do metody wymagającej Class<ArrayList<SomeType>>.

Możesz rzucić obiekt na odpowiedni typ:

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

To da pewne ostrzeżenia o niebezpiecznych rzutach i oczywiście twój ArgumentCaptor nie może tak naprawdę rozróżniać między elementami ArrayList<SomeType>i ArrayList<AnotherType>nie sprawdzać ich.

(Jak wspomniano w drugiej odpowiedzi, chociaż jest to ogólny problem ogólny, istnieje specyficzne dla Mockito rozwiązanie problemu bezpieczeństwa typu z @Captoradnotacją. Wciąż nie można odróżnić an ArrayList<SomeType>i ArrayList<OtherType>.)

Edytować:

Zobacz także komentarz tenshi . Możesz zmienić oryginalny kod z Paŭlo Ebermann na ten (znacznie prostszy)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Paŭlo Ebermann
źródło
49
Pokazany przykład można uprościć w oparciu o fakt, że java wnioskuje o typie dla statycznych wywołań metod:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenshi
4
Aby wyłączyć ostrzeżenie o użyciu niezaznaczonych lub niebezpiecznych operacji , należy użyć @SuppressWarnings("unchecked")adnotacji nad linią definicji captor argumentu. Ponadto przesyłanie do Classjest zbędne.
mrts
1
Przesyłanie do Classnie jest zbędne w moich testach.
Wim Deblauwe,
16

Jeśli nie boisz się starej semantyki w stylu java (nie typowa bezpieczna generyczna), to również działa i jest dość proste:

ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
verify(subject.method(argument.capture()); // run your code
List<SomeType> list = argument.getValue(); // first captured List, etc.
rogerdpack
źródło
2
Możesz dodać @SuppressWarnings („surowe typy”) przed deklaracją wyłączenia ostrzeżeń.
pkalinow
9
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"));
kkmike999
źródło
4

W oparciu o komentarze @ tenshi i @ pkalinow (również kudos do @rogerdpack), poniższe jest proste rozwiązanie do tworzenia modułu przechwytującego argument listy, który również wyłącza ostrzeżenie „używa operacji niesprawdzonych lub niebezpiecznych” :

@SuppressWarnings("unchecked")
final ArgumentCaptor<List<SomeType>> someTypeListArgumentCaptor =
    ArgumentCaptor.forClass(List.class);

Pełny przykład tutaj i odpowiadająca mu wersja kompilacji CI i uruchomienie testowe tutaj .

Nasz zespół używa tego od jakiegoś czasu w naszych testach jednostkowych i wydaje się to dla nas najprostszym rozwiązaniem.

mrts
źródło
2

W przypadku wcześniejszej wersji programu junit możesz to zrobić

Class<Map<String, String>> mapClass = (Class) Map.class;
ArgumentCaptor<Map<String, String>> mapCaptor = ArgumentCaptor.forClass(mapClass);
quzhi65222714
źródło
1

Miałem ten sam problem z testowaniem aktywności w mojej aplikacji na Androida. Użyłem ActivityInstrumentationTestCase2i MockitoAnnotations.initMocks(this);nie działałem. Rozwiązałem ten problem z inną klasą, odpowiednio z polem. Na przykład:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

        public CaptorHolder() {
            MockitoAnnotations.initMocks(this);
        }
    }

Następnie w metodzie testu aktywności:

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();
Timofey Orischenko
źródło
0

W GitHub Mockito istnieje otwarty problem dotyczący tego konkretnego problemu.

Znalazłem proste obejście, które nie zmusza cię do używania adnotacji w swoich testach:

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);
        }

    }

}

Co się dzieje, jest to, że możemy stworzyć nową klasę z tej @Captoradnotacji i wstrzyknąć captor do niego. Następnie po prostu wydobywamy porywacz i zwracamy go z naszej metody statycznej.

W teście możesz użyć go w następujący sposób:

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

Lub ze składnią podobną do Jacksona TypeReference:

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

Działa, ponieważ Mockito tak naprawdę nie potrzebuje żadnych informacji o typie (na przykład w przeciwieństwie do serializatorów).

Jezor
źródło