Przewodnik praktyka testów jednostkowych po codziennym mockito

Opublikowany: 2022-03-11

Testy jednostkowe stały się obowiązkowe w dobie Agile i dostępnych jest wiele narzędzi, które pomagają w automatycznym testowaniu. Jednym z takich narzędzi jest Mockito, framework open source, który pozwala tworzyć i konfigurować mockowane obiekty do testów.

W tym artykule omówimy tworzenie i konfigurowanie mocków oraz wykorzystywanie ich do weryfikacji oczekiwanego zachowania testowanego systemu. Zagłębimy się również trochę w wnętrze Mockito, aby lepiej zrozumieć jego projekt i zastrzeżenia. Będziemy używać JUnit jako frameworka do testów jednostkowych, ale ponieważ Mockito nie jest powiązany z JUnit, możesz podążać dalej, nawet jeśli używasz innego frameworka.

Uzyskanie Mockito

W dzisiejszych czasach zdobycie Mockito jest łatwe. Jeśli używasz Gradle, wystarczy dodać ten pojedynczy wiersz do skryptu kompilacji:

 testCompile "org.mockito:mockito−core:2.7.7"

Jeśli chodzi o takich jak ja, którzy nadal wolą Mavena, po prostu dodaj Mockito do swoich zależności w następujący sposób:

 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>

Oczywiście świat jest znacznie szerszy niż Maven i Gradle. Możesz użyć dowolnego narzędzia do zarządzania projektami, aby pobrać artefakt Mockito jar z centralnego repozytorium Maven.

Zbliżanie się do Mockito

Testy jednostkowe są przeznaczone do testowania zachowania określonych klas lub metod bez polegania na zachowaniu ich zależności. Ponieważ testujemy najmniejszą „jednostkę” kodu, nie musimy używać rzeczywistych implementacji tych zależności. Ponadto użyjemy nieco innych implementacji tych zależności podczas testowania różnych zachowań. Tradycyjne, dobrze znane podejście do tego polega na tworzeniu „odcinków” – specyficznych implementacji interfejsu odpowiedniego dla danego scenariusza. Takie implementacje mają zwykle zakodowaną logikę. Odgałęzienie jest rodzajem dubletu testowego. Inne rodzaje to podróbki, drwiny, szpiedzy, manekiny itp.

Skoncentrujemy się tylko na dwóch rodzajach dublerów testowych, „kwotach” i „szpiegach”, ponieważ są one intensywnie wykorzystywane przez Mockito.

Mocks

Czym jest kpina? Oczywiście nie jest to miejsce, w którym wyśmiewasz się z innych programistów. Mockowanie do testów jednostkowych ma miejsce wtedy, gdy tworzysz obiekt, który implementuje zachowanie rzeczywistego podsystemu w kontrolowany sposób. Krótko mówiąc, mocki są używane jako zamiennik zależności.

Z Mockito tworzysz mock, mówisz Mockito, co ma zrobić, gdy wywoływane są na nim określone metody, a następnie używasz instancji mock w swoim teście zamiast rzeczywistej. Po teście można odpytać mock, aby zobaczyć jakie konkretnie metody zostały wywołane lub sprawdzić skutki uboczne w postaci zmienionego stanu.

Domyślnie Mockito zapewnia implementację dla każdej metody makiety.

Szpiedzy

Szpieg to inny rodzaj testowego sobowtóra, który tworzy Mockito. W przeciwieństwie do drwin, tworzenie szpiega wymaga szpiegowania instancji. Domyślnie szpieg deleguje wszystkie wywołania metod do obiektu rzeczywistego i rejestruje, jaka metoda została wywołana i z jakimi parametrami. To właśnie czyni go szpiegiem: szpieguje prawdziwy przedmiot.

Rozważ używanie drwin zamiast szpiegów, gdy tylko jest to możliwe. Szpiedzy mogą być użyteczni do testowania starszego kodu, którego nie można przeprojektować tak, aby był łatwy do przetestowania, ale potrzeba użycia szpiega do częściowego ośmieszenia klasy jest wskaźnikiem, że klasa robi za dużo, naruszając w ten sposób zasadę pojedynczej odpowiedzialności.

Budowanie prostego przykładu

Rzućmy okiem na proste demo, do którego możemy pisać testy. Załóżmy, że mamy interfejs UserRepository z pojedynczą metodą wyszukiwania użytkownika według jego identyfikatora. Mamy również koncepcję kodera hasła, który przekształca hasło w postaci zwykłego tekstu w skrót hasła. Zarówno UserRepository , jak i PasswordEncoder są zależnościami (zwanymi również współpracownikami) UserService wstrzykiwanymi za pośrednictwem konstruktora. Oto jak wygląda nasz kod demo:

Repozytorium użytkowników
 public interface UserRepository { User findById(String id); }
Użytkownik
 public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
Koder hasła
 public interface PasswordEncoder { String encode(String password); }
Obsługa użytkownika
 public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }

Ten przykładowy kod można znaleźć na GitHub, więc możesz go pobrać do recenzji wraz z tym artykułem.

Stosowanie Mockito

Korzystając z naszego przykładowego kodu, spójrzmy, jak zastosować Mockito i napisać kilka testów.

Tworzenie Mocków

W Mockito tworzenie makiety jest tak proste, jak wywołanie statycznej metody Mockito.mock() :

 import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);

Zwróć uwagę na import statyczny dla Mockito. W pozostałej części tego artykułu domyślnie rozważymy dodanie tego importu.

Po imporcie wyśmiewamy PasswordEncoder , interfejs. Mockito kpi nie tylko z interfejsów, ale także z klas abstrakcyjnych i konkretnych klas nieostatecznych. Po wyjęciu z pudełka, Mockito nie może mocować końcowych klas i końcowych lub statycznych metod, ale jeśli naprawdę tego potrzebujesz, Mockito 2 zapewnia eksperymentalną wtyczkę MockMaker.

Należy również zauważyć, że metod equals() i hashCode() nie można naśladować.

Tworzenie szpiegów

Aby utworzyć szpiega, musisz wywołać metodę statyczną spy() Mockito i przekazać jej instancję do szpiegowania. Wywołanie metod zwróconego obiektu spowoduje wywołanie metod rzeczywistych, chyba że metody te są zastępowane. Wywołania te są nagrywane, a fakty tych wywołań można zweryfikować (patrz dalszy opis verify() ). Zróbmy szpiega:

 DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));

Tworzenie szpiega nie różni się zbytnio od robienia szyderstwa. Co więcej, wszystkie metody Mockito używane do konfiguracji makiety mają również zastosowanie do konfiguracji szpiega.

Szpiedzy są rzadko wykorzystywani w porównaniu do mocków, ale mogą okazać się przydatne do testowania starszego kodu, którego nie można zrefaktoryzować, gdzie testowanie wymaga częściowego naśladowania. W takich przypadkach możesz po prostu utworzyć szpiega i zablokować niektóre z jego metod, aby uzyskać pożądane zachowanie.

Domyślne wartości zwrotu

Wywołanie mock(PasswordEncoder.class) zwraca wystąpienie PasswordEncoder . Możemy nawet wywołać jego metody, ale co one zwrócą? Domyślnie wszystkie metody atrapy zwracają „niezainicjowane” lub „puste” wartości, np. zera dla typów numerycznych (zarówno pierwotnych, jak i opakowanych), false dla wartości logicznych i null dla większości innych typów.

Rozważ następujący interfejs:

 interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }

Rozważmy teraz następujący fragment, który daje wyobrażenie o tym, jakich wartości domyślnych można oczekiwać od metod makiety:

 Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());

Metody karczowania

Świeże, niezmienione makiety są przydatne tylko w rzadkich przypadkach. Zwykle chcemy skonfigurować mock i zdefiniować, co zrobić, gdy wywoływane są określone metody makiety. Nazywa się to karczowaniem .

Mockito oferuje dwa sposoby na kark. Pierwszy sposób to „ kiedy wywoływana jest ta metoda, zrób coś”. Rozważ następujący fragment:

 when(passwordEncoder.encode("1")).thenReturn("a");

Brzmi prawie jak angielski: „Kiedy wywoływana jest nazwa passwordEncoder.encode(“1”) , zwróć a .”

Drugi sposób oznaczania jest bardziej podobny do „Zrób coś, gdy ta metoda makiety jest wywoływana z następującymi argumentami”. Ten sposób karczowania jest trudniejszy do odczytania, ponieważ przyczyna jest określona na końcu. Rozważać:

 doReturn("a").when(passwordEncoder).encode("1");

Fragment kodu z tą metodą oznaczania to: „Zwróć a , gdy metoda encode() passwordEncoder jest wywoływana z argumentem 1 ”.

Pierwszy sposób jest uważany za preferowany, ponieważ jest bezpieczny dla czcionek i jest bardziej czytelny. Rzadko jednak jest się zmuszonym do skorzystania z drugiego sposobu, na przykład przy ogłuszaniu prawdziwej metody szpiega, ponieważ wywołanie jej może mieć niepożądane skutki uboczne.

Przyjrzyjmy się pokrótce metodom tubbingu dostarczonym przez Mockito. W naszych przykładach uwzględnimy oba sposoby oznaczania.

Zwracanie wartości

thenReturn lub doReturn() służą do określenia wartości, która ma zostać zwrócona po wywołaniu metody.

 //”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");

lub

 //”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");

Możesz również określić wiele wartości, które zostaną zwrócone jako wyniki kolejnych wywołań metod. Ostatnia wartość zostanie użyta w wyniku dla wszystkich dalszych wywołań metod.

 //when when(passwordEncoder.encode("1")).thenReturn("a", "b");

lub

 //do doReturn("a", "b").when(passwordEncoder).encode("1");

To samo można osiągnąć za pomocą następującego fragmentu kodu:

 when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");

Ten wzorzec może być również używany z innymi metodami zastępowania w celu zdefiniowania wyników kolejnych wywołań.

Zwracanie odpowiedzi niestandardowych

then() , alias do thenAnswer() i doAnswer() osiągają to samo, co jest konfiguracją niestandardowej odpowiedzi, która ma zostać zwrócona po wywołaniu metody, na przykład:

 when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");

lub

 doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");

Jedynym argumentem, jaki przyjmuje thenAnswer() , jest implementacja interfejsu Answer . Ma pojedynczą metodę z parametrem typu InvocationOnMock .

Możesz również zgłosić wyjątek w wyniku wywołania metody:

 when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });

…lub wywołaj prawdziwą metodę klasy (nie dotyczy interfejsów):

 Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());

Masz rację, jeśli uważasz, że wygląda to nieporęcznie. Mockito udostępnia thenCallRealMethod() i thenThrow() w celu usprawnienia tego aspektu testowania.

Wywoływanie prawdziwych metod

Jak sama nazwa wskazuje, thenCallRealMethod() i doCallRealMethod() wywołują metodę real na obiekcie makiety:

 Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());

Wywołanie rzeczywistych metod może być przydatne w częściowych mockach, ale upewnij się, że wywoływana metoda nie ma niepożądanych skutków ubocznych i nie zależy od stanu obiektu. Jeśli tak, szpieg może być lepszym rozwiązaniem niż kpiarz.

Jeśli stworzysz makietę interfejsu i spróbujesz skonfigurować skrót do wywołania rzeczywistej metody, Mockito wyrzuci wyjątek z bardzo pouczającym komunikatem. Rozważ następujący fragment:

 when(passwordEncoder.encode("1")).thenCallRealMethod();

Mockito zawiedzie z następującym komunikatem:

 Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();

Uznanie dla twórców Mockito za troskę na tyle, aby zapewnić tak dokładne opisy!

Rzucanie wyjątków

thenThrow() i doThrow() konfigurują wyśmiewaną metodę, aby zgłosić wyjątek:

 when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());

lub

 doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");

Mockito zapewnia, że ​​zgłaszany wyjątek jest poprawny dla tej konkretnej metody zastępczej i będzie narzekał, jeśli wyjątek nie znajduje się na liście sprawdzonych wyjątków metody. Rozważ następujące:

 when(passwordEncoder.encode("1")).thenThrow(new IOException());

Doprowadzi to do błędu:

 org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException

Jak widać, Mockito wykrył, że encode() nie może zgłosić wyjątku IOException .

Możesz również przekazać klasę wyjątku zamiast przekazywać instancję wyjątku:

 when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);

lub

 doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");

To powiedziawszy, Mockito nie może sprawdzać poprawności klasy wyjątku w taki sam sposób, w jaki sprawdza instancję wyjątku, więc musisz zachować dyscyplinę i nie przekazywać niedozwolonych obiektów klas. Na przykład poniższy wyjątek zgłosi wyjątek IOException , chociaż nie oczekuje się, że metoda encode() zgłosi sprawdzony wyjątek:

 when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");

Wyśmiewanie interfejsów za pomocą domyślnych metod

Warto zauważyć, że podczas tworzenia makiety interfejsu Mockito kpi ze wszystkich metod tego interfejsu. Od wersji Java 8 interfejsy mogą zawierać metody domyślne wraz z metodami abstrakcyjnymi. Te metody są również wyszydzane, więc musisz uważać, aby działały jako metody domyślne.

Rozważmy następujący przykład:

 interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());

W tym przykładzie assertFalse() zakończy się sukcesem. Jeśli nie tego się spodziewałeś, upewnij się, że Mockito wywołało metodę rzeczywistą, na przykład:

 AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());

Dopasowywanie argumentów

W poprzednich sekcjach skonfigurowaliśmy nasze mockowane metody z dokładnymi wartościami jako argumentami. W takich przypadkach Mockito po prostu wywołuje wewnętrznie equals() , aby sprawdzić, czy oczekiwane wartości są równe wartościom rzeczywistym.

Czasami jednak nie znamy tych wartości z góry.

Może po prostu nie dbamy o rzeczywistą wartość przekazywaną jako argument, a może chcemy zdefiniować reakcję na szerszy zakres wartości. Wszystkie te scenariusze (i nie tylko) można rozwiązać za pomocą dopasowywania argumentów. Pomysł jest prosty: zamiast podawać dokładną wartość, zapewniasz dopasowanie argumentów, aby Mockito dopasowywał argumenty metody.

Rozważ następujący fragment:

 when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));

Widać, że wynik jest taki sam bez względu na to, jaką wartość przekażemy do encode() , ponieważ użyliśmy dopasowywania argumentów anyString() w pierwszej linii. Jeśli przepiszemy tę linię zwykłym angielskim, brzmiałoby to tak: „kiedy koder hasła zostanie poproszony o zakodowanie dowolnego ciągu, zwróć ciąg„dokładnie”.

Mockito wymaga podania wszystkich argumentów za pomocą dopasowań lub dokładnych wartości. Więc jeśli metoda ma więcej niż jeden argument i chcesz użyć dopasowywania argumentów tylko dla niektórych jej argumentów, zapomnij o tym. Nie możesz napisać kodu w ten sposób:

 abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);

Aby naprawić błąd, musimy zastąpić ostatnią linię, aby zawierała dopasowanie argumentów eq dla a , w następujący sposób:

 when(mock.call(eq("a"), anyInt())).thenReturn(true);

Tutaj użyliśmy dopasowywania argumentów eq() i anyInt() , ale dostępnych jest wiele innych. Aby uzyskać pełną listę dopasowywania argumentów, zapoznaj się z dokumentacją dotyczącą klasy org.mockito.ArgumentMatchers .

Należy pamiętać, że nie można używać dopasowywania argumentów poza weryfikacją lub skrótem. Na przykład nie możesz mieć:

 //this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);

Mockito wykryje źle umieszczony element dopasowujący argument i InvalidUseOfMatchersException . Weryfikację z dopasowywaniem argumentów należy przeprowadzić w ten sposób:

 verify(mock).encode(or(eq("a"), endsWith("b")));

Dopasowania argumentów również nie mogą być używane jako wartość zwracana. Mockito nie może zwrócić anyString() lub any-cokolwiek; dokładna wartość jest wymagana w przypadku połączeń pośredniczących.

Niestandardowe dopasowania

Niestandardowe dopasowania przychodzą na ratunek, gdy musisz zapewnić logikę dopasowania, która nie jest jeszcze dostępna w Mockito. Nie należy lekceważyć decyzji o utworzeniu niestandardowego programu dopasowującego, ponieważ konieczność dopasowania argumentów w nietrywialny sposób wskazuje na problem w projekcie lub na to, że test staje się zbyt skomplikowany.

W związku z tym warto sprawdzić, czy można uprościć test, używając niektórych łagodnych dopasowywania argumentów, takich jak isNull() i nullable() , przed napisaniem niestandardowego dopasowywania. Jeśli nadal czujesz potrzebę napisania dopasowywania argumentów, Mockito zapewnia rodzinę metod, aby to zrobić.

Rozważmy następujący przykład:

 FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));

Tutaj tworzymy element dopasowujący argument hasLuck i używamy argThat() , aby przekazać element dopasowujący jako argument do wykpiwanej metody, odcinając go, aby zwrócił true , jeśli nazwa pliku kończy się na „szczęście”. Możesz traktować ArgumentMatcher jako funkcjonalny interfejs i utworzyć jego instancję za pomocą lambdy (co zrobiliśmy w przykładzie). Mniej zwięzła składnia wyglądałaby tak:

 ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };

Jeśli potrzebujesz utworzyć dopasowywanie argumentów, które działa z typami pierwotnymi, istnieje kilka innych metod w org.mockito.ArgumentMatchers :

  • charThat(ArgumentMatcher<znak> dopasowanie)
  • booleanThat(ArgumentMatcher<Boolean> dopasowanie)
  • byteThat(ArgumentMatcher<Byte> matcher)
  • shortThat(ArgumentMatcher<Short> matcher)
  • intThat(ArgumentMatcher<Integer> dopasowanie)
  • longThat(ArgumentMatcher<Long> matcher)
  • floatThat(ArgumentMatcher<Float> dopasowanie)
  • doubleThat(ArgumentMatcher<Double> matcher)

Łączenie dopasowanych

Nie zawsze warto tworzyć niestandardowe dopasowanie argumentów, gdy warunek jest zbyt skomplikowany, aby można go było obsłużyć za pomocą podstawowych dopasowań; czasami łączenie pasujących załatwi sprawę. Mockito zapewnia dopasowywanie argumentów do implementacji typowych operacji logicznych („nie”, „i”, „lub”) na dopasowywaniu argumentów, które pasują zarówno do typów pierwotnych, jak i nieprymitywnych. Te dopasowania są implementowane jako metody statyczne w klasie org.mockito.AdditionalMatchers .

Rozważmy następujący przykład:

 when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));

Tutaj połączyliśmy wyniki dwóch dopasowań argumentów: eq("1") i contains("a") . Końcowe wyrażenie, or(eq("1"), contains("a")) , może być interpretowane jako „ciąg argumentów musi być równy „1” lub zawierać „a”.

Zauważ, że w klasie org.mockito.AdditionalMatchers są wymienione mniej popularne elementy dopasowujące, takie jak geq() , leq() , gt() i lt() , które są porównaniami wartości mającymi zastosowanie do wartości pierwotnych i wystąpień języka java.lang.Comparable .

Weryfikowanie zachowania

Po użyciu makiety lub szpiega możemy verify , czy miały miejsce określone interakcje. Dosłownie mówimy „Hej, Mockito, upewnij się, że ta metoda została wywołana z tymi argumentami”.

Rozważmy następujący sztuczny przykład:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");

Tutaj stworzyliśmy mock i wywołaliśmy jego metodę encode() . Ostatnia linia weryfikuje, czy metoda encode() makiety została wywołana z określoną wartością argumentu a . Należy pamiętać, że weryfikacja wywołania z kodem zastępczym jest zbędna; celem poprzedniego fragmentu jest pokazanie pomysłu przeprowadzenia weryfikacji po wystąpieniu pewnych interakcji.

Jeśli zmienimy ostatnią linię na inny argument — powiedzmy b — poprzedni test zakończy się niepowodzeniem i Mockito będzie narzekał, że faktyczne wywołanie ma inne argumenty ( b zamiast oczekiwanego a ).

Argumentów dopasowujących można używać do weryfikacji, podobnie jak do skrótu:

 verify(passwordEncoder).encode(anyString());

Domyślnie Mockito weryfikuje, czy metoda została wywołana raz, ale możesz zweryfikować dowolną liczbę wywołań:

 // verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());

Rzadko używaną cechą verify() jest zdolność do niepowodzenia w przypadku przekroczenia limitu czasu, co jest przydatne głównie do testowania współbieżnego kodu. Na przykład, jeśli nasz koder hasła jest wywoływany w innym wątku jednocześnie z verify() , możemy napisać test w następujący sposób:

 usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");

Ten test powiedzie się, jeśli encode() zostanie wywołana i zakończona w ciągu 500 milisekund lub mniej. Jeśli musisz odczekać cały określony okres, użyj after() zamiast timeout() :

 verify(passwordEncoder, after(500)).encode("a");

Inne tryby weryfikacji ( times() , atLeast() , itp.) można łączyć z timeout() i after() w celu wykonania bardziej skomplikowanych testów:

 // passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");

Oprócz times() , obsługiwane tryby weryfikacji obejmują only() , atLeast() i atLeastOnce() (jako alias do atLeast(1) ).

Mockito umożliwia również weryfikację kolejności połączeń w grupie makiet. Nie jest to funkcja, z której można często korzystać, ale może być przydatna, jeśli ważna jest kolejność wywołań. Rozważmy następujący przykład:

 PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");

Jeśli zmienimy kolejność symulowanych wywołań, test zakończy się niepowodzeniem z VerificationInOrderFailure .

Brak wywołań można również zweryfikować za pomocą verifyZeroInteractions() . Ta metoda akceptuje mock lub mocki jako argument i nie powiedzie się, jeśli jakiekolwiek metody przekazane w mock(ach) zostały wywołane.

Warto również wspomnieć o verifyNoMoreInteractions() , która przyjmuje mocki jako argument i może służyć do sprawdzania, czy każde wywołanie tych mocków zostało zweryfikowane.

Przechwytywanie argumentów

Oprócz sprawdzania, czy metoda została wywołana z określonymi argumentami, Mockito umożliwia przechwycenie tych argumentów, dzięki czemu można później uruchomić na nich niestandardowe asercje. Innymi słowy, mówisz „Hej, Mockito, sprawdź, czy ta metoda została wywołana i podaj mi wartości argumentów, z którymi została wywołana”.

Stwórzmy makiety PasswordEncoder , wywołajmy encode() , przechwyćmy argument i sprawdźmy jego wartość:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());

Jak widać, przekazujemy passwordCaptor.capture() jako argument encode() do weryfikacji; to wewnętrznie tworzy dopasowywanie argumentów, które zapisuje argument. Następnie pobieramy przechwyconą wartość za pomocą passwordCaptor.getValue() i sprawdzamy ją za pomocą assertEquals() .

Jeśli musimy przechwycić argument w wielu wywołaniach, ArgumentCaptor pozwala pobrać wszystkie wartości za pomocą getAllValues() , na przykład:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());

Tej samej techniki można użyć do przechwytywania argumentów metody o zmiennej arności (znanych również jako varargs).

Testowanie naszego prostego przykładu

Teraz, gdy wiemy dużo więcej o Mockito, czas wrócić do naszego demo. Napiszmy test metody isValidUser . Oto jak to może wyglądać:

 public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }

Nurkowanie pod API

Mockito zapewnia czytelny, wygodny interfejs API, ale przyjrzyjmy się niektórym jego wewnętrznym funkcjom, aby zrozumieć jego ograniczenia i uniknąć dziwnych błędów.

Przyjrzyjmy się, co dzieje się w Mockito, gdy uruchamiany jest następujący fragment:

 // 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));

Oczywiście pierwsza linia tworzy kpinę. Mockito używa ByteBuddy do stworzenia podklasy danej klasy. Nowy obiekt klasy ma wygenerowaną nazwę, taką jak demo.mockito.PasswordEncoder$MockitoMock$1953422997 , jego equals() będzie sprawdzać tożsamość, a hashCode() zwróci kod skrótu tożsamości. Po wygenerowaniu i załadowaniu klasy jej instancja jest tworzona przy użyciu Objenesis.

Spójrzmy na następną linię:

 when(mock.encode("a")).thenReturn("1");

Kolejność jest ważna: pierwsza instrukcja wykonywana tutaj to mock.encode("a") , która wywoła encode() na makiecie z domyślną wartością zwracaną null . Tak naprawdę przekazujemy null jako argument funkcji when() . Mockito nie dba o to, jaka dokładna wartość jest przekazywana do when() , ponieważ przechowuje informacje o wywołaniu sfałszowanej metody w tzw. Później, kiedy wywołujemy when() , Mockito ściąga ten trwający obiekt zastępujący i zwraca go jako wynik when() . Następnie wywołujemy thenReturn(“1”) na zwróconym trwającym obiekcie skrótu.

Trzecia linia, mock.encode("a"); jest proste: wywołujemy metodę skrótową. Wewnętrznie Mockito zapisuje to wywołanie do dalszej weryfikacji i zwraca skróconą odpowiedź na wywołanie; w naszym przypadku jest to ciąg 1 .

W czwartym wierszu ( verify(mock).encode(or(eq("a"), endsWith("b"))); ) prosimy Mockito o sprawdzenie, czy nastąpiło wywołanie encode() z tymi konkretne argumenty.

Verify verify() jest wykonywane jako pierwsze, co zamienia wewnętrzny stan Mockito w tryb weryfikacji. Ważne jest, aby zrozumieć, że Mockito zachowuje swój stan w ThreadLocal . Umożliwia to zaimplementowanie ładnej składni, ale z drugiej strony może prowadzić do dziwnego zachowania, jeśli framework jest używany niewłaściwie (na przykład, jeśli próbujesz użyć dopasowywania argumentów poza weryfikacją lub skrótem).

Jak więc Mockito tworzy or dopasowuje? Najpierw wywoływane jest eq("a") , a do stosu dopasowujących dodawany jest element dopasowujący equals się. Po drugie, wywoływane jest endsWith("b") , a do stosu dodawany jest element endsWith . W końcu wywoływane jest or(null, null) — używa dwóch elementów dopasowujących, które zdejmuje ze stosu, tworzy element dopasowujący or i odkłada go na stos. Na koniec wywoływana jest encode() . Mockito następnie sprawdza, czy metoda została wywołana oczekiwaną liczbę razy i z oczekiwanymi argumentami.

Chociaż dopasowania argumentów nie mogą być wyodrębnione do zmiennych (ponieważ zmienia to kolejność wywołań), można je wyodrębnić do metod. Zachowuje to kolejność połączeń i utrzymuje stos we właściwym stanie:

 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }

Zmiana odpowiedzi domyślnych

W poprzednich sekcjach stworzyliśmy nasze mocki w taki sposób, że gdy wywoływane są jakiekolwiek mocki, zwracają one „pustą” wartość. To zachowanie jest konfigurowalne. Możesz nawet podać własną implementację org.mockito.stubbing.Answer , jeśli te dostarczone przez Mockito nie są odpowiednie, ale może to wskazywać, że coś jest nie tak, gdy testy jednostkowe stają się zbyt skomplikowane. Pamiętaj o zasadzie KISS!

Przyjrzyjmy się ofercie predefiniowanych domyślnych odpowiedzi Mockito:

  • RETURNS_DEFAULTS to strategia domyślna; nie warto o tym wyraźnie wspominać podczas tworzenia makiety.

  • CALLS_REAL_METHODS sprawia, że ​​niezatwierdzone wywołania wywołują prawdziwe metody.

  • RETURNS_SMART_NULLS pozwala uniknąć NullPointerException , zwracając SmartNull zamiast null podczas korzystania z obiektu zwróconego przez wywołanie metody bez kodu pośredniczącego. Nadal zakończy się niepowodzeniem z NullPointerException , ale SmartNull zapewnia ładniejszy ślad stosu z wierszem, w którym wywołano metodę unstubbed. To sprawia, że ​​warto mieć RETURNS_SMART_NULLS jako domyślną odpowiedź w Mockito!

  • RETURNS_MOCKS najpierw próbuje zwrócić zwykłe „puste” wartości, potem kpi, jeśli to możliwe, i null w przeciwnym razie. Kryteria pustki różnią się nieco od tego, co widzieliśmy wcześniej: zamiast zwracania null dla łańcuchów i tablic, mocki utworzone za pomocą RETURNS_MOCKS zwracają odpowiednio puste łańcuchy i puste tablice.

  • RETURNS_SELF przydaje się do wyśmiewania budowniczych. Przy tym ustawieniu makieta zwróci swoją instancję, jeśli zostanie wywołana metoda, która zwróci coś w rodzaju równego klasie (lub nadklasie) imitowanej klasy.

  • RETURNS_DEEP_STUBS sięga głębiej niż RETURNS_MOCKS i tworzy mocki, które są w stanie zwrócić mocki z mocków z mocków itp. W przeciwieństwie do RETURNS_MOCKS , reguły pustki są domyślne w RETURNS_DEEP_STUBS , więc zwraca null dla łańcuchów i tablic:

 interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());

Nazywanie Mocka

Mockito pozwala nazwać mock, funkcja przydatna, jeśli w teście masz dużo mocków i musisz je rozróżnić. To powiedziawszy, konieczność nazywania kpin może być objawem złego projektu. Rozważ następujące:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());

Mockito będzie narzekał, ale ponieważ nie nazwaliśmy formalnie kpin, nie wiemy, który:

 Wanted but not invoked: passwordEncoder.encode(<any string>);

Nazwijmy je, przekazując ciąg znaków podczas budowy:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());

Teraz komunikat o błędzie jest bardziej przyjazny i wyraźnie wskazuje na robustPasswordEncoder :

 Wanted but not invoked: robustPasswordEncoder.encode(<any string>);

Implementing Multiple Mock Interfaces

Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:

 PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);

Listening Invocations

A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.

 InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");

In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger (don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging() setting:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());

Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Rozważmy następujący przykład:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");

This snippet will produce an output similar to the following:

 ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"

Note that the first logged invocation corresponds to calling encode() while stubbing it. It's the next invocation that corresponds to calling the stubbed method.

Other Settings

Mockito offers a few more settings that let you do the following:

  • Enable mock serialization by using withSettings().serializable() .
  • Turn off recording of method invocations to save memory (this will make verification impossible) by using withSettings().stubOnly() .
  • Use the constructor of a mock when creating its instance by using withSettings().useConstructor() . When mocking inner non-static classes, add an outerInstance() setting, like so: withSettings().useConstructor().outerInstance(outerObject) .

If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance() setting, so that Mockito will create a spy on the instance you provide, like so:

 UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));

When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.

Note that, when you create a spy, you're basically creating a mock that calls real methods:

 // creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));

When Mockito Tastes Bad

It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.

Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.

Clearing Invocations

Mockito allows for clearing invocations for mocks while preserving stubbing, like so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);

Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.

Resetting a Mock

Resetting a mock with reset() is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.

Overusing Verify

Another bad habit is trying to replace every assert with Mockito's verify() . It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify() , while confirming the observable results of an executed action is done with asserts.

Mockito Is about Frame of Mind

Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.

With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.