Kiedy używać Mockito.verify ()?

201

Piszę przypadki testowe jUnit dla 3 celów:

  1. Aby upewnić się, że mój kod spełnia wszystkie wymagane funkcje, we wszystkich (lub w większości) kombinacjach / wartościach wejściowych.
  2. Aby upewnić się, że mogę zmienić implementację i polegać na testach JUnit, aby powiedzieć, że cała moja funkcjonalność jest nadal zadowolona.
  3. Jako dokumentacja wszystkich przypadków użycia mój kod obsługuje i działa jako specyfikacja do refaktoryzacji - na wypadek, gdyby kod kiedykolwiek musiał zostać przepisany. (Zmodyfikuj kod, a jeśli moje testy jUnit zakończą się niepowodzeniem - prawdopodobnie przegapiłeś jakiś przypadek użycia).

Nie rozumiem, dlaczego i kiedy Mockito.verify()należy go użyć. Kiedy widzę verify(), że ktoś mnie wzywa, mówi mi, że mój JUnit zaczyna być świadomy implementacji. (W ten sposób zmiana mojej implementacji spowodowałaby uszkodzenie moich JUnits, mimo że moja funkcjonalność nie uległa zmianie).

Szukam:

  1. Jakie powinny być wytyczne dotyczące właściwego użytkowania Mockito.verify()?

  2. Czy zasadniczo poprawne jest, aby jUnits był świadomy implementacji testowanej klasy lub był ściśle z nią związany?

Russell
źródło
1
Staram się unikać używania Verify () tak bardzo, jak potrafię, z tego samego powodu, który ujawniłeś (nie chcę, aby mój test jednostkowy był świadomy implementacji), ale zdarzają się przypadki, gdy nie mam wyboru - zatarte metody pustki. Mówiąc ogólnie, ponieważ nie zwracają niczego, nie przyczyniają się do twoich „rzeczywistych” wyników; ale musisz wiedzieć, że tak się nazywało. Ale zgadzam się z tobą, że nie ma sensu używać weryfikacji do weryfikacji przepływu wykonania.
Legna

Odpowiedzi:

78

Jeśli umowa klasy A obejmuje fakt, że wywołuje metodę B obiektu typu C, należy to przetestować, wykonując próbkę typu C i sprawdzając, czy metoda B została wywołana.

Oznacza to, że umowa klasy A ma wystarczającą szczegółowość, że mówi o typie C (który może być interfejsem lub klasą). Tak, mówimy o poziomie specyfikacji, który wykracza poza „wymagania systemowe” i w pewien sposób opisuje sposób implementacji.

Jest to normalne w przypadku testów jednostkowych. Podczas testów jednostkowych chcesz mieć pewność, że każda jednostka robi „właściwą rzecz”, a to zwykle obejmuje interakcje z innymi jednostkami. „Jednostki” mogą tutaj oznaczać klasy lub większe podzbiory aplikacji.

Aktualizacja:

Wydaje mi się, że nie dotyczy to tylko weryfikacji, ale także stubowania. Gdy tylko wprowadzisz metodę klasy współpracującej, test jednostkowy w pewnym sensie zależy od implementacji. Tak jest w rodzaju testów jednostkowych. Ponieważ w Mockito chodzi zarówno o stubowanie, jak i o weryfikację, fakt, że używasz Mockito w ogóle oznacza, że ​​będziesz miał do czynienia z tego rodzaju zależnością.

Z mojego doświadczenia wynika, że ​​jeśli zmieniam implementację klasy, często muszę zmieniać implementację testów jednostkowych, aby ją dopasować. Zazwyczaj, choć nie będę musiał zmienić wykaz co jednostka testy nie dla klasy; chyba że powodem zmiany było istnienie warunku, którego wcześniej nie testowałem.

Właśnie o to chodzi w testach jednostkowych. Test, który nie cierpi z powodu tego rodzaju zależności od sposobu wykorzystania klas współpracowników, jest tak naprawdę testem podsystemu lub testem integracji. Oczywiście są one często pisane przy użyciu JUnit i często wymagają użycia szyderstwa. Moim zdaniem „JUnit” to okropna nazwa dla produktu, który pozwala nam produkować różnego rodzaju testy.

Dawood ibn Kareem
źródło
8
Dzięki, David. Po zeskanowaniu niektórych zestawów kodów wydaje się to powszechną praktyką - ale dla mnie nie wystarcza to do tworzenia testów jednostkowych, a jedynie dodaje narzut związany z utrzymywaniem ich dla bardzo małej wartości. Rozumiem, dlaczego wymagane są symulacje i dlaczego należy skonfigurować zależności do wykonania testu. Ale sprawdzenie, czy ta metoda jest zależna. XYZ () powoduje, że testy są bardzo kruche, moim zdaniem.
Russell
@ Russell Nawet jeśli „typ C” jest interfejsem dla opakowania wokół biblioteki lub innego odrębnego podsystemu twojej aplikacji?
Dawood ibn Kareem
1
Nie powiedziałbym, że całkowicie bezużyteczne jest zapewnienie wywołania jakiegoś podsystemu lub usługi - wystarczy, że powinny istnieć pewne wytyczne (sformułowanie ich było tym, co chciałem zrobić). Na przykład: (prawdopodobnie przesadzam z tym) Powiedzmy, że używam StrUtil.equals () w moim kodzie i decyduję się przejść na StrUtil.equalsIgnoreCase () w implementacji. Jeśli jUnit miał weryfikację (StrUtil.equals ), mój test może się nie powieść, chociaż implementacja jest dokładna. To sprawdzanie połączenia, IMO, jest złą praktyką, chociaż dotyczy bibliotek / podsystemów. Z drugiej strony użycie funkcji Verify w celu upewnienia się, że wywołanie funkcji closeDbConn może być prawidłowym przypadkiem użycia.
Russell
1
Rozumiem cię i całkowicie się z tobą zgadzam. Ale czuję też, że napisanie opisanych przez ciebie wytycznych może przerodzić się w napisanie całego podręcznika TDD lub BDD. Na przykład, wywołanie equals()lub equalsIgnoreCase()nigdy nie byłoby czymś określonym w wymaganiach klasy, więc nigdy nie miałby testu jednostkowego per se. Jednak „zamknięcie połączenia DB po zakończeniu” (cokolwiek to oznacza pod względem implementacji) może być wymogiem klasy, nawet jeśli nie jest to „wymaganie biznesowe”. Dla mnie sprowadza się to do relacji między umową ...
Dawood ibn Kareem,
... klasy wyrażonej w wymaganiach biznesowych oraz zestawu metod testowych, które testują tę klasę. Zdefiniowanie tej relacji byłoby ważnym tematem w każdej książce na temat TDD lub BDD. Podczas gdy ktoś z zespołu Mockito mógłby napisać post na ten temat na swojej wiki, nie widzę, czym różni się on od wielu innych dostępnych literatur. Jeśli widzisz, jak to może się różnić, daj mi znać, a może będziemy mogli nad tym popracować.
Dawood ibn Kareem
60

Odpowiedź Davida jest oczywiście poprawna, ale nie wyjaśnia, dlaczego tak chcesz.

Zasadniczo podczas testowania jednostek testujesz jednostkę funkcjonalności w izolacji. Testujesz, czy dane wejściowe dają oczekiwane wyniki. Czasami musisz również przetestować działania niepożądane. Krótko mówiąc, weryfikacja pozwala to zrobić.

Na przykład masz trochę logiki biznesowej, która ma przechowywać rzeczy za pomocą DAO. Można to zrobić za pomocą testu integracji, który tworzy instancję DAO, łączy ją z logiką biznesową, a następnie przeszukuje bazę danych, aby sprawdzić, czy oczekiwane rzeczy zostały zapisane. To już nie jest test jednostkowy.

Możesz też kpić z DAO i sprawdzić, czy zostanie wywołany w oczekiwany sposób. Za pomocą mockito możesz sprawdzić, czy coś jest wywoływane, jak często jest ono wywoływane, a nawet używać dopasowań parametrów, aby upewnić się, że jest wywoływane w określony sposób.

Drugą stroną takich testów jednostkowych jest to, że wiążesz testy z implementacją, co utrudnia refaktoryzację. Z drugiej strony, dobry zapach projektu to ilość kodu potrzebna do jego prawidłowego wykonania. Jeśli twoje testy muszą być bardzo długie, prawdopodobnie coś jest nie tak z projektem. Więc kod z wieloma efektami ubocznymi / złożonymi interakcjami, które należy przetestować, prawdopodobnie nie jest dobrą rzeczą.

Jilles van Gurp
źródło
29

To świetne pytanie! Myślę, że podstawowa przyczyna tego jest następująca: używamy JUnit nie tylko do testowania jednostkowego. Pytanie powinno zostać podzielone:

  • Czy powinienem używać Mockito.verify () w mojej integracji? (lub innych testach jednostkowych)?
  • Czy powinienem używać Mockito.verify () w testach jednostkowych w czarnej skrzynce ?
  • Czy powinienem używać Mockito.verify () w testach jednostkowych białych skrzynek ?

więc jeśli zignorujemy testy wyższe niż jednostkowe, pytanie można sformułować ponownie: „ Używanie białych testów jednostkowych za pomocą Mockito.verify () tworzy świetną parę między testem jednostkowym a moją możliwą implementacją, czy mogę zrobić trochę „ szarej skrzynki „ testowanie jednostkowe i jakie podstawowe zasady powinienem zastosować do tego ”.

Teraz przejdźmy przez cały ten krok po kroku.

* - Czy powinienem używać Mockito.verify () w mojej integracji (lub innych testach wyższych niż jednostkowe)? * Myślę, że odpowiedź brzmi zdecydowanie nie, poza tym nie powinieneś używać do tego mocków. Twój test powinien być jak najbardziej zbliżony do rzeczywistej aplikacji. Testujesz pełny przypadek użycia, a nie wydzieloną część aplikacji.

* Testowanie jednostkowe czarnej skrzynki a białej skrzynki * Jeśli używasz metody czarnej skrzynki do tego, co naprawdę robisz, podajesz (wszystkie klasy równoważności) dane wejściowe, stan i testy, które otrzymasz oczekiwany wynik. W tym podejściu stosowanie mocków w ogóle jest uzasadnione (po prostu naśladujesz, że robią to dobrze; nie chcesz ich testować), ale wywołanie Mockito.verify () jest zbyteczne.

Jeśli stosujesz podejście „ białej skrzynki”, co naprawdę robisz, testujesz zachowanie swojego urządzenia. W tym podejściu niezbędne jest wywołanie Mockito.verify (), powinieneś sprawdzić, czy twoje urządzenie zachowuje się tak, jak się tego spodziewasz.

podstawowe zasady testowania szarych skrzynek Problem z testami szarych skrzynek polega na tym, że tworzy ono wysokie sprzęganie. Jednym z możliwych rozwiązań jest przeprowadzenie testu szarej skrzynki, a nie testowanie białej skrzynki. Jest to swego rodzaju połączenie testów czarnych i białych skrzynek. Naprawdę testujesz zachowanie swojego urządzenia, tak jak w przypadku testów białych skrzynek, ale ogólnie czynisz go agnostycznym, jeśli to możliwe . Gdy jest to możliwe, po prostu sprawdzisz, jak w przypadku czarnej skrzynki, po prostu zapewnisz, że wynik jest tym, czego się spodziewasz. Istota twojego pytania brzmi: kiedy jest to możliwe.

To jest naprawdę trudne. Nie mam dobrego przykładu, ale mogę podać przykłady. W przypadku, o którym wspomniano powyżej za pomocą equals () vs equalsIgnoreCase (), nie powinieneś wywoływać Mockito.verify (), po prostu sprawdź dane wyjściowe. Jeśli nie możesz tego zrobić, podziel kod na mniejsze jednostki, dopóki nie będziesz w stanie tego zrobić. Z drugiej strony, załóżmy, że masz trochę @Service i piszesz @ Web-Service, który jest zasadniczo opakowany w twoją @Service - deleguje wszystkie wywołania do @Service (i robi dodatkową obsługę błędów). W takim przypadku wywołanie Mockito.verify () jest niezbędne, nie powinieneś powielać wszystkich swoich kontroli wykonanych dla @Serive, wystarczające jest sprawdzenie, czy dzwonisz do @Service z poprawną listą parametrów.

alexsmail
źródło
Testy w szarej skrzynce to trochę pułapka. Zazwyczaj ograniczam to do rzeczy takich jak DAO. Pracowałem nad niektórymi projektami z bardzo powolnymi kompilacjami z powodu dużej liczby testów szarych skrzynek, prawie całkowitego braku testów jednostkowych i zbyt wielu testów czarnej skrzynki, aby zrekompensować brak zaufania do tego, co rzekomo testowały testy greybox.
Jilles van Gurp
Dla mnie jest to najlepsza dostępna odpowiedź, ponieważ odpowiada, kiedy używać Mockito.when () w różnych sytuacjach. Dobra robota.
Michiel Leegwater
8

Muszę powiedzieć, że masz absolutną rację z punktu widzenia klasycznego podejścia:

  • Jeśli najpierw utworzysz (lub zmienisz) logikę biznesową swojej aplikacji, a następnie pokryjesz ją (przyjmiesz) testami ( podejście Test-Last ), to bardzo bolesne i niebezpieczne będzie powiadomienie testów o tym, jak działa twoje oprogramowanie, poza sprawdzanie wejść i wyjść.
  • Jeśli ćwiczysz podejście oparte na testach, to testy są najpierw pisane, zmieniane i odzwierciedlają przypadki użycia funkcji oprogramowania. Implementacja zależy od testów. To czasami oznacza, że ​​chcesz, aby twoje oprogramowanie było implementowane w określony sposób, np. Polegaj na metodzie innego komponentu lub nawet nazywaj to określoną ilość razy. To tam Mockito.verify () przydaje!

Należy pamiętać, że nie ma uniwersalnych narzędzi. Rodzaj oprogramowania, jego wielkość, cele firmy i sytuacja rynkowa, umiejętności zespołu i wiele innych rzeczy wpływają na decyzję, które podejście zastosować w konkretnym przypadku.

hammelion
źródło
0

Jak powiedzieli niektórzy ludzie

  1. Czasami nie masz bezpośredniego wyjścia, na którym można by potwierdzić
  2. Czasami musisz tylko potwierdzić, że Twoja przetestowana metoda wysyła prawidłowe pośrednie dane wyjściowe do swoich współpracowników (które kpisz).

Jeśli chodzi o obawy związane z przełamaniem testów podczas refaktoryzacji, jest to nieco oczekiwane w przypadku korzystania z próbnych / odcinków / szpiegów. Mam na myśli to z definicji, a nie konkretnej implementacji, takiej jak Mockito. Ale można myśleć w ten sposób - jeśli trzeba zrobić refaktoryzacji która powodowałaby znaczne zmiany na sposób działania metody, to jest dobry pomysł, aby zrobić to na podejściu TDD, dzięki czemu można zmienić testu najpierw do zdefiniowania nowe zachowanie (które zakończy się niepowodzeniem testu), a następnie dokonaj zmian i ponownie uzyskaj pozytywny wynik testu.

Emanuel Luiz Lariguet Beltrame
źródło
0

W większości przypadków, gdy ludzie nie lubią korzystać z Mockito.verify, dzieje się tak, ponieważ służy on do weryfikacji wszystkiego, co robi testowana jednostka, a to oznacza, że ​​będziesz musiał dostosować test, jeśli coś się w nim zmieni. Ale nie sądzę, że to jest problem. Jeśli chcesz mieć możliwość zmiany działania metody bez konieczności zmiany jej testu, oznacza to w zasadzie, że chcesz pisać testy, które nie sprawdzają wszystkiego, co robi twoja metoda, ponieważ nie chcesz, aby testowała twoje zmiany . I to jest zły sposób myślenia.

Naprawdę problemem jest to, czy możesz zmodyfikować to, co robi twoja metoda, a test jednostkowy, który ma całkowicie obejmować tę funkcję, nie zawiedzie. Oznaczałoby to, że bez względu na cel zmiany, wynik tej zmiany nie jest objęty testem.

Z tego powodu wolę wyśmiewać jak najwięcej: wyśmiewaj także swoje obiekty danych. Wykonując tę ​​czynność, możesz nie tylko użyć polecenia Sprawdź, aby sprawdzić, czy wywoływane są prawidłowe metody innych klas, ale także czy przekazywane dane są gromadzone za pomocą prawidłowych metod tych obiektów danych. Aby to zakończyć, należy przetestować kolejność wykonywania połączeń. Przykład: jeśli zmodyfikujesz obiekt encji db, a następnie zapiszesz go za pomocą repozytorium, nie wystarczy zweryfikować, czy seteri obiektu są wywoływane z poprawnymi danymi i czy wywoływana jest metoda zapisu repozytorium. Jeśli są wywoływane w niewłaściwej kolejności, twoja metoda nadal nie robi tego, co powinna. Tak więc nie używam Mockito.verify, ale tworzę obiekt inOrder ze wszystkimi próbami i zamiast tego używam inOrder.verify. A jeśli chcesz to zrobić, powinieneś również zadzwonić do Mockito. ZweryfikujNoMoreInteractions na końcu i przekaż mu wszystkie symulacje. W przeciwnym razie ktoś może dodać nową funkcjonalność / zachowanie bez testowania jej, co oznacza, że ​​po tym czasie statystyki pokrycia mogą wynosić 100%, a Ty nadal gromadzisz kod, który nie jest potwierdzony ani zweryfikowany.

Stefan Mondelaers
źródło