Czy kpiny naruszają zasadę otwartej / zamkniętej?

13

Jakiś czas temu przeczytałem na odpowiedzi Przepełnienie stosu, której nie mogę znaleźć, zdanie wyjaśniające, że powinieneś przetestować publiczne interfejsy API, a autor powiedział, że powinieneś przetestować interfejsy. Autor wyjaśnił również, że jeśli zmieni się implementacja metody, nie trzeba modyfikować przypadku testowego, ponieważ spowoduje to zerwanie umowy zapewniającej działanie testowanego systemu. Innymi słowy, test powinien zakończyć się niepowodzeniem, jeśli metoda nie działa, ale nie dlatego, że implementacja uległa zmianie.

Zwróciło to moją uwagę, gdy mówimy o kpinach. Ponieważ wyśmiewanie polega w dużej mierze na wywołaniach oczekujących z testowanych zależności systemu, symulacje są ściśle powiązane z implementacją, a nie z interfejsem.

Podczas badań próbnych i próbnych , kilka artykułów zgadza się, że zamiast próbnych powinny być stosowane kody pośredniczące, ponieważ nie opierają się one na oczekiwaniach wynikających z zależności, co oznacza, że ​​test nie wymaga znajomości bazowego systemu w trakcie testowania.

Moje pytania brzmiałyby:

  1. Czy kpiny naruszają zasadę otwartego / zamkniętego?
  2. Czy w ostatnim argumencie brakuje czegoś na korzyść kodów pośredniczących, które sprawiają, że kody pośredniczące nie są tak świetne w porównaniu do próbnych?
  3. Jeśli tak, to kiedy byłby to dobry przypadek użycia do wyszydzenia, a kiedy byłby dobry przypadek użycia do użycia kodów pośredniczących?
Christopher Francisco
źródło
8
Since mocking relays heavily on expectation calls from system under test's dependencies...Myślę, że właśnie tam się nie udajesz. Kpina to sztuczna reprezentacja systemu zewnętrznego. Nie reprezentuje w żaden sposób systemu zewnętrznego, z wyjątkiem przypadków, gdy symuluje system zewnętrzny w taki sposób, że pozwala na uruchomienie testów z kodem zależnym od tego systemu zewnętrznego. Nadal będziesz potrzebować testów integracyjnych, aby udowodnić, że Twój kod działa z prawdziwym, niezamkniętym systemem.
Robert Harvey,
8
Innymi słowy, makieta jest implementacją zastępczą. Dlatego w pierwszej kolejności zaprogramowaliśmy interfejs, abyśmy mogli używać makiet jako stand-in dla prawdziwej implementacji. Innymi słowy, symulacje są oddzielone od rzeczywistej implementacji , a nie od niej powiązane.
Robert Harvey,
3
„Innymi słowy, test powinien zakończyć się niepowodzeniem, jeśli metoda nie działa, ale nie dlatego, że implementacja uległa zmianie”, nie zawsze jest to prawda. Istnieje wiele okoliczności, w których należy zmienić zarówno implementację, jak i testy.
whatsisname

Odpowiedzi:

4
  1. Nie rozumiem, dlaczego drwiny naruszałyby zasadę otwartego / zamkniętego. Jeśli możesz nam wyjaśnić, dlaczego według ciebie mogą, to możemy złagodzić twoje obawy.

  2. Jedyną wadą kodów pośredniczących, o których mogę myśleć, jest to, że na ogół wymagają więcej pracy niż pisanie próbne, ponieważ każdy z nich jest w rzeczywistości alternatywną implementacją interfejsu zależnego, więc na ogół musi zapewniać pełną (lub przekonująco kompletną) implementacja interfejsu zależnego. Aby dać ci skrajny przykład, jeśli testowany podsystem wywołuje RDBMS, wówczas próbka RDBMS po prostu odpowiada na określone zapytania, o których wiadomo, że są wysyłane przez testowany podsystem, dając z góry określone zestawy danych testowych. Z drugiej strony alternatywną implementacją byłby w pełni funkcjonalny RDBMS w pamięci, być może z dodatkowym obciążeniem związanym z koniecznością emulacji dziwactw rzeczywistego klienta-serwera RDBMS, którego używasz na produkcji. (Na szczęście mamy takie rzeczy jak HSQLDB, więc właściwie możemy to zrobić, ale nadal,

  3. Dobrymi przykładami użycia kpienia są sytuacje, w których interfejs zależny jest zbyt skomplikowany, aby napisać dla niego alternatywną implementację lub jeśli masz pewność, że napiszesz makietę tylko raz i nigdy jej nie dotkniesz. W takich przypadkach skorzystaj z szybkiej i brudnej makiety. W związku z tym dobre przypadki użycia kodów pośredniczących (implementacje alternatywne) to właściwie wszystko inne. Zwłaszcza jeśli przewidujesz długoterminowe relacje z testowanym podsystemem, zdecydowanie wybierz alternatywną implementację, która będzie ładna i czysta i będzie wymagać konserwacji tylko w przypadku zmiany interfejsu, zamiast wymagać konserwacji za każdym razem, gdy interfejs zmiany i za każdym razem, gdy zmienia się wdrażanie testowanego podsystemu.

PS Osoba, o której mówisz, może być mną, w jednej z moich innych odpowiedzi związanych z testowaniem tutaj na programmers.stackexchange.com, na przykład tej .

Mike Nakis
źródło
an alternative implementation would be a full-blown in-memory RDBMS- Nie musisz koniecznie iść tak daleko z kikutem.
Robert Harvey
@RobertHarvey dobrze, z HSQLDB i H2 nie jest tak trudno dotrzeć tak daleko. Prawdopodobnie trudniej jest zrobić coś na wpół osaczonego, aby nie posunąć się tak daleko. Ale jeśli zdecydujesz się zrobić to sam, musisz zacząć od napisania parsera SQL. Jasne, możesz skrócić rogi, ale jest dużo pracy . W każdym razie, jak powiedziałem powyżej, jest to tylko skrajny przykład.
Mike Nakis,
9
  1. Zasada Otwarta / Zamknięta polega przede wszystkim na możliwości zmiany zachowania klasy bez modyfikacji. Dlatego wstrzyknięcie fałszywej zależności komponentu do testowanej klasy nie narusza jej.

  2. Problem z podwójnymi testami (makiety / odgałęzienia) polega na tym, że w zasadzie przyjmujesz arbitralne założenia dotyczące tego, w jaki sposób testowana klasa oddziałuje z otoczeniem. Jeśli te oczekiwania są błędne, po wdrożeniu kodu mogą wystąpić problemy. Jeśli możesz sobie na to pozwolić, przetestuj kod w tych samych ograniczeniach, które ograniczają środowisko produkcyjne. Jeśli nie możesz, podejmij najmniejsze możliwe założenia i próbuj / stub tylko peryferia twojego systemu (baza danych, usługa uwierzytelniania, klient HTTP itp.).

Jedynym słusznym powodem, dla którego należy użyć IMHO, podwójnego, jest to, kiedy trzeba zarejestrować jego interakcje z testowaną klasą lub gdy trzeba podać fałszywe dane (co mogą zrobić obie techniki). Bądź jednak ostrożny, nadużywanie go odzwierciedla zły projekt lub test, który w zbyt dużym stopniu opiera się na testowanym interfejsie API.

Francis Toth
źródło
6

Uwaga: Zakładam, że definiujesz Mock jako „klasę bez implementacji, po prostu coś, co możesz monitorować”, a Stub jako „częściową próbę, czyli używa niektórych rzeczywistych zachowań zaimplementowanej klasy”, zgodnie z tym stosem Pytanie o przepełnieniu .

Nie jestem pewien, dlaczego uważasz, że konsensus polega na użyciu kodów pośredniczących , na przykład w dokumentacji Mockito jest odwrotnie.

Jak zwykle przeczytasz częściowe fałszywe ostrzeżenie: Programowanie obiektowe w mniejszym stopniu rozwiązuje złożoność, dzieląc ją na osobne, specyficzne obiekty SRPy. Jak częściowa makieta pasuje do tego paradygmatu? Cóż, to po prostu nie… Częściowa makieta zwykle oznacza, że ​​złożoność została przeniesiona do innej metody na tym samym obiekcie. W większości przypadków nie jest to sposób projektowania aplikacji.

Jednak zdarzają się rzadkie przypadki, w których przydają się częściowe makiety: radzenie sobie z kodem, którego nie można łatwo zmienić (interfejsy stron trzecich, tymczasowe refaktoryzacja starszego kodu itp.) Jednak nie użyłbym częściowych makiet dla nowych, testowanych i dobrze zaprojektowany kod.

Ta dokumentacja mówi to lepiej niż ja. Używanie prób pozwala ci tylko przetestować jedną konkretną klasę i nic więcej; jeśli potrzebujesz częściowych prób, aby osiągnąć pożądane zachowanie, prawdopodobnie zrobiłeś coś złego, naruszasz SRP i tak dalej, a Twój kod może stać się refaktorem. Makiety nie naruszają zasady otwartego zamknięcia, ponieważ i tak są one zawsze używane tylko w testach, nie są prawdziwymi zmianami w tym kodzie. Zwykle i tak są generowane w locie przez bibliotekę taką jak cglib.

durron597
źródło
2
Z tego samego dostarczonego pytania SO (zaakceptowana odpowiedź), to jest definicja Mock / Stub, o której również mówiłem: Obiekty próbne są używane do definiowania oczekiwań, tj .: W tym scenariuszu oczekuję, że metoda A () zostanie wywołana z takimi i takimi parametrami. Egzaminy rejestrują i weryfikują takie oczekiwania. Z drugiej strony, odcinki mają inny cel: nie rejestrują ani nie weryfikują oczekiwań, ale pozwalają nam „zastąpić” zachowanie, stan „fałszywego” obiektu w celu wykorzystania scenariusza testowego ...
Christopher Francisco
2

Myślę, że problem może wynikać z założenia, że ​​jedynymi prawidłowymi testami są te, które spełniają test otwarty / zamknięty.

Łatwo zauważyć, że jedynym testem, który powinien mieć znaczenie, jest testowanie interfejsu. Jednak w rzeczywistości często bardziej efektywne jest testowanie tego interfejsu poprzez testowanie wewnętrznych mechanizmów.

Na przykład prawie niemożliwe jest przetestowanie jakichkolwiek negatywnych wymagań, takich jak „wdrożenie nie spowoduje żadnych wyjątków”. Rozważ implementację interfejsu mapy z hashapem. Chcesz mieć pewność, że mapa skrótów spełnia interfejs mapy, bez rzucania, nawet jeśli trzeba ją przerobić (co może stać się ryzykowne). Możesz przetestować każdą kombinację danych wejściowych, aby upewnić się, że spełniają one wymagania dotyczące interfejsu, ale może to potrwać dłużej niż śmierć cieplna wszechświata. Zamiast tego przerywasz nieco enkapsulację i opracowujesz symulacje, które oddziałują ściślej, zmuszając hashap do wykonania dokładnie powtórzenia potrzebnego, aby algorytm powtórzenia nie rzucił.

Tl / Dr: robienie tego „przy książce” jest fajne, ale kiedy przychodzi do popchnięcia, posiadanie produktu na biurku szefa do piątku jest bardziej przydatne niż testowy zestaw książek, który trwa do śmierci termicznej wszechświat, aby potwierdzić zgodność.

Cort Ammon
źródło