Jak działają dopasowania Mockito?

122

Dopasowujących Mockito argument (takie jak any, argThat, eq, samei ArgumentCaptor.capture()) zachowują się inaczej dopasowujących Hamcrest.

  • Dopasowania Mockito często powodują wyjątek InvalidUseOfMatchersException, nawet w kodzie, który jest wykonywany długo po użyciu jakichkolwiek dopasowań.

  • Dopasowania Mockito podlegają dziwnym regułom, takim jak wymaganie użycia dopasowań Mockito tylko dla wszystkich argumentów, jeśli jeden argument w danej metodzie używa dopasowania.

  • Dopasowania Mockito mogą powodować wyjątek NullPointerException podczas zastępowania Answers lub używania (Integer) any()itp.

  • Refaktoryzacja kodu za pomocą dopasowań Mockito w określony sposób może powodować wyjątki i nieoczekiwane zachowanie i może całkowicie zawieść.

Dlaczego dopasowujące Mockito są zaprojektowane w ten sposób i jak są wdrażane?

Jeff Bowman
źródło

Odpowiedzi:

236

Mockito to metody statyczne i wywołania tych metod, które zastępują argumenty podczas wywołań funkcji wheni verify.

Dopasowywanie Hamcresta (wersja zarchiwizowana) (lub dopasowywanie w stylu Hamcresta) to bezstanowe instancje obiektów ogólnego przeznaczenia, które implementują Matcher<T>i ujawniają metodę, matches(T)która zwraca wartość true, jeśli obiekt spełnia kryteria dopasowania. Mają być wolne od skutków ubocznych i są zwykle używane w twierdzeniach, takich jak poniższe.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Dopasowania Mockito istnieją niezależnie od dopasowań w stylu Hamcresta, więc opisy wyrażeń dopasowujących pasują bezpośrednio do wywołań metod : dopasowujące Mockito zwracają Ttam, gdzie metody dopasowujące Hamcrest zwracają obiekty dopasowujące (typuMatcher<T> .

Dopasowujących Mockito są wywoływane za pomocą metod statycznych, takich jak eq, any,gt , i startsWithna org.mockito.Matchersi org.mockito.AdditionalMatchers. Istnieją również adaptery, które zmieniły się w wersjach Mockito:

  • W przypadku Mockito 1.x, Matchersniektóre wywołania (takie jak intThatlub argThat) są dopasowaniami Mockito, które bezpośrednio akceptują dopasowania Hamcrest jako parametry.ArgumentMatcher<T>rozszerzony org.hamcrest.Matcher<T>, który był używany w wewnętrznej reprezentacji Hamcrest i był podstawową klasą dopasowującą Hamcrest zamiast dowolnego rodzaju Mockito.
  • W przypadku Mockito 2.0+ Mockito nie jest już bezpośrednio zależne od Hamcresta. Matcherswywołania wyrażone jako intThatlub argThatzawijające ArgumentMatcher<T>obiekty, które już nie implementują, org.hamcrest.Matcher<T>ale są używane w podobny sposób. Adaptery do ścięgien, takie jak argThatiintThat są nadal dostępne, ale MockitoHamcrestzamiast tego przeniosły się .

Niezależnie od tego, czy dopasowujące są ścięgnami, czy po prostu w stylu Hamcrest, można je dostosować w następujący sposób:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

W powyższym stwierdzeniu: foo.setPowerLevelto metoda, która akceptuje rozszerzenie int. is(greaterThan(9000))zwraca a Matcher<Integer>, co nie działałoby jako setPowerLevelargument. Dopasowanie Mockito intThatowija to dopasowanie w stylu Hamcrest i zwraca inttak to mógł pojawić się jako argument; Mockito dopasowujące się, takie jakgt(9000) całe wyrażenie w jedno wywołanie, tak jak w pierwszym wierszu przykładowego kodu.

Co dopasowują / zwracają

when(foo.quux(3, 5)).thenReturn(true);

Kiedy nie używa dopasowywania argumentów, Mockito rejestruje wartości argumentów i porównuje je z ich equalsmetodami.

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

Kiedy dzwonisz do dopasowującego, takiego jak any lub gt(większe niż), Mockito przechowuje obiekt dopasowujący, który powoduje, że Mockito pomija sprawdzanie równości i stosuje wybrane dopasowanie. W przypadkuargumentCaptor.capture() tego przechowuje dopasowania, które zamiast tego zapisuje swój argument do późniejszej inspekcji.

Dopasowania zwracają fikcyjne wartości, takie jak zero, puste kolekcje lub null. Mockito próbuje zwrócić bezpieczną, odpowiednią wartość fikcyjną, taką jak 0 dla anyInt()lub any(Integer.class)lub pusty List<String>dla anyListOf(String.class). Jednak z powodu wymazywania typów Mockito nie ma informacji o typie, aby zwrócić jakąkolwiek wartość oprócz nullfor any()lubargThat(...) , co może spowodować NullPointerException jeśli próbuje „Auto-unbox” a nullpierwotną wartość.

Mecze lubią eq i gtprzyjmują wartości parametrów; w idealnym przypadku wartości te powinny zostać obliczone przed rozpoczęciem tworzenia kodu pośredniczącego / weryfikacji. Wywołanie kpiny w trakcie kpiny z innego połączenia może kolidować z klepaniem.

Metody dopasowujące nie mogą być używane jako wartości zwracane; nie ma sposobu na wyrażenie thenReturn(anyInt())lubthenReturn(any(Foo.class)) w Mockito. Mockito musi dokładnie wiedzieć, do której instancji powrócić w wywołaniach pośredniczących i nie wybierze dla Ciebie arbitralnej wartości zwracanej.

Szczegóły dotyczące wdrożenia

Dopasowania są przechowywane (jako elementy dopasowujące obiekty w stylu Hamcrest) w stosie zawartym w klasie o nazwie ArgumentMatcherStorage . MockitoCore i Matchers posiadają instancję ThreadSafeMockingProgress , która statycznie zawiera ThreadLocal przechowującą instancje MockingProgress. To ten MockingProgressImpl zawiera konkret ArgumentMatcherStorageImpl . W związku z tym stan makiety i dopasowywania jest statyczny, ale spójny w zakresie wątku między klasami Mockito i Matchers.

Większość połączeń Matcher tylko dodać do tego stosu, z wyjątkiem dla dopasowujących jak and, orinot . To doskonale odpowiada (i polega na) kolejności oceny w Javie , która ocenia argumenty od lewej do prawej przed wywołaniem metody:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

To będzie:

  1. Dodaj anyInt()do stosu.
  2. Dodaj gt(10)do stosu.
  3. Dodaj lt(20)do stosu.
  4. Usunąć gt(10)i lt(20)dodać and(gt(10), lt(20)).
  5. Call foo.quux(0, 0), które (o ile nie jest to skrótowe) zwraca wartość domyślną false. Wewnętrznie Mockito oznacza quux(int, int)ostatnie połączenie.
  6. Call when(false), które odrzuca swój argument i przygotowuje do metody stub quux(int, int)określonej w 5. Jedyne dwa prawidłowe stany mają długość stosu 0 (równość) lub 2 (dopasowywanie), a na stosie są dwa dopasowania (kroki 1 i 4), więc Mockito odcina metodę z any()dopasowaniem dla pierwszego argumentu i and(gt(10), lt(20))drugiego argumentu i czyści stos.

To pokazuje kilka zasad:

  • Mockito nie potrafi odróżnić quux(anyInt(), 0)i quux(0, anyInt()). Oba wyglądają jak wezwanie do quux(0, 0)z jednym dopasowaniem int na stosie. W konsekwencji, jeśli używasz jednego dopasowania, musisz dopasować wszystkie argumenty.

  • Kolejność połączeń jest nie tylko ważna, ale sprawia, że ​​wszystko działa . Wyodrębnianie dopasowań do zmiennych zwykle nie działa, ponieważ zwykle zmienia kolejność wywołań. Jednak wyodrębnianie dopasowań do metod działa świetnie.

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • Stos zmienia się na tyle często, że Mockito nie może go bardzo dokładnie nadzorować. Może sprawdzać stos tylko podczas interakcji z Mockito lub makietą i musi akceptować dopasowania, nie wiedząc, czy zostaną użyte natychmiast, czy przypadkowo porzucone. Teoretycznie stos powinien być zawsze pusty poza wywołaniem whenlub verify, ale Mockito nie może tego sprawdzić automatycznie. Możesz sprawdzić ręcznie za pomocą Mockito.validateMockitoUsage().

  • W wywołaniu funkcji whenMockito w rzeczywistości wywołuje daną metodę, co spowoduje zgłoszenie wyjątku, jeśli metoda została skrócona, aby zgłosić wyjątek (lub wymaga wartości niezerowych lub niezerowych). doReturni doAnswer(itp.) nie wywołują rzeczywistej metody i często są użyteczną alternatywą.

  • Gdybyś eqwywołał metodę pozorowaną w trakcie odgrywania kodu (np. W celu obliczenia odpowiedzi dla dopasowania), Mockito sprawdziłby zamiast tego długość stosu w porównaniu z tym wywołaniem i prawdopodobnie się nie uda.

  • Jeśli spróbujesz zrobić coś złego, na przykład zgarnąć / zweryfikować ostateczną metodę , Mockito wywoła prawdziwą metodę, a także pozostawi dodatkowe dopasowania na stosie . finalWywołanie metody mogą nie wyjątek, ale może pojawić się InvalidUseOfMatchersException od bezpańskich dopasowujących przy następnym interakcji z mock.

Częste problemy

  • InvalidUseOfMatchersException :

    • Sprawdź, czy każdy argument ma dokładnie jedno wywołanie dopasowujące, jeśli w ogóle używasz dopasowań i czy nie użyłeś dopasowania poza wywołaniem whenlub verify. Dopasowania nigdy nie powinny być używane jako skrócone wartości zwracane lub pola / zmienne.

    • Sprawdź, czy nie wywołujesz mocka jako części argumentu dopasowującego.

    • Sprawdź, czy nie próbujesz odgiąć / zweryfikować ostatecznej metody za pomocą dopasowania. To świetny sposób na pozostawienie dopasowania na stosie i chyba że Twoja ostatnia metoda rzuci wyjątek, może to być jedyny moment, w którym zdasz sobie sprawę, że metoda, z której kpisz, jest ostateczna.

  • NullPointerException z argumentami pierwotnymi: (Integer) any() zwraca null, podczas gdy any(Integer.class)zwraca 0; może to spowodować, NullPointerExceptionjeśli spodziewasz się an intzamiast liczby całkowitej. W każdym razie preferuj anyInt(), co zwróci zero, a także pominie krok automatycznego pakowania.

  • NullPointerException lub inne wyjątki: domaga się when(foo.bar(any())).thenReturn(baz)rzeczywiście nazwać foo.bar(null) , co może masz zgaszone rzucić wyjątek podczas odbierania zerowy argument. Przełączenie na doReturn(baz).when(foo).bar(any()) pomija zachowanie skrótów .

Ogólne rozwiązywanie problemów

  • Użyj MockitoJUnitRunner lub jawnie wywołaj validateMockitoUsagew swojej metodzie tearDownlub @After(co runner zrobi za Ciebie automatycznie). Pomoże to ustalić, czy nadużyłeś dopasowań.

  • W celu debugowania dodaj wywołania do validateMockitoUsagebezpośrednio w kodzie. Spowoduje to wyrzucenie, jeśli masz coś na stosie, co jest dobrym ostrzeżeniem o złym objawie.

Jeff Bowman
źródło
2
Dzięki za tę wiadomość. Wyjątek NullPointerException z formatem when / thenReturn sprawiał mi problemy, dopóki nie zmieniłem go na doReturn / when.
yngwietiger
11

To tylko mały dodatek do doskonałej odpowiedzi Jeffa Bowmana, ponieważ znalazłem to pytanie, szukając rozwiązania jednego z moich własnych problemów:

Jeśli wywołanie metody pasuje do więcej niż jednego whenwytrenowanego wywołania pozorowanego , kolejność whenwywołań jest ważna i powinna być od najszerszej do najbardziej szczegółowej. Zaczynając od jednego z przykładów Jeffa:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

to kolejność zapewniająca (prawdopodobnie) pożądany rezultat:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Jeśli odwrócisz wywołania when, wynik zawsze będzie true.

tibtof
źródło
2
Chociaż jest to przydatna informacja, dotyczy to stubbingu, a nie dopasowania , więc może nie mieć sensu w tym pytaniu. Porządek ma znaczenie, ale tylko wtedy, gdy wygrywa ostatnio zdefiniowany łańcuch dopasowania : Oznacza to, że współistniejące kody pośredniczące są często deklarowane jako najbardziej specyficzne lub najmniejsze, ale w niektórych przypadkach możesz chcieć bardzo szerokiego zastąpienia specjalnie wyśmiewanego zachowania w pojedynczym przypadku testowym , w którym to momencie szeroka definicja może być ostatnią.
Jeff Bowman
1
@JeffBowman Pomyślałem, że ma to sens w przypadku tego pytania, ponieważ pytanie dotyczy dopasowań mockito, a dopasowania mogą być używane podczas stubbowania (jak w większości twoich przykładów). Ponieważ szukanie w Google wyjaśnienia doprowadziło mnie do tego pytania, myślę, że warto mieć te informacje tutaj.
tibtof