Czy warto przeprowadzać testy jednostkowe, które usuwają i kpią z wszystkiego, co publiczne?

59

Kiedy wykonuję testy jednostkowe w „właściwy” sposób, tj. Przerywając każde publiczne połączenie i zwracając ustawione wartości lub kpiny, mam wrażenie, że tak naprawdę niczego nie testuję. Dosłownie patrzę na mój kod i tworzę przykłady oparte na logice za pomocą moich publicznych metod. I za każdym razem, gdy zmienia się implementacja, muszę iść i zmieniać te testy, ponownie, nie czując, że osiągam coś użytecznego (czy to w perspektywie średnio-, czy długoterminowej). Robię również testy integracyjne (w tym ścieżki nie-szczęśliwe) i nie mam nic przeciwko wydłużonym czasom testowania. W przypadku tych mam wrażenie, że faktycznie testuję pod kątem regresji, ponieważ wykryli wiele, podczas gdy wszystkie testy jednostkowe pokazują, że zmieniła się implementacja mojej publicznej metody, o której już wiem.

Testy jednostkowe to rozległy temat i wydaje mi się, że nie rozumiem tutaj czegoś. Jaka jest decydująca zaleta testów jednostkowych w porównaniu z testami integracyjnymi (wyłączając czasochłonność)?

entropowie
źródło
4
Moje dwa centy: Nie nadużywaj kpin
Juan Mendes,
1
Zobacz także „Kpina to zapach kodu”
user949300,

Odpowiedzi:

37

Kiedy wykonuję testy jednostkowe w „właściwy” sposób, tj. Przerywając każde publiczne połączenie i zwracając ustawione wartości lub kpiny, mam wrażenie, że tak naprawdę niczego nie testuję. Dosłownie patrzę na mój kod i tworzę przykłady oparte na logice za pomocą moich publicznych metod.

Wygląda na to, że metoda, którą testujesz, potrzebuje kilku innych instancji klas (które musisz wyśmiewać) i sama wywołuje kilka metod.

Ten rodzaj kodu jest rzeczywiście trudny do testowania jednostkowego z powodów, które przedstawisz.

Pomocne jest podzielenie takich klas na:

  1. Zajęcia z rzeczywistą „logiką biznesową”. Wykorzystują one niewiele lub nie wywołują żadnych innych połączeń i są łatwe do przetestowania (wartość (wartości) poza wartością).
  2. Klasy współpracujące z systemami zewnętrznymi (pliki, baza danych itp.). Zawierają one system zewnętrzny i zapewniają wygodny interfejs dla twoich potrzeb.
  3. Klasy, które „wiążą wszystko razem”

Następnie klasy od 1. są łatwe do testowania jednostkowego, ponieważ akceptują tylko wartości i zwracają wynik. W bardziej skomplikowanych przypadkach klasy te mogą wymagać samodzielnego wykonywania wywołań, ale będą wywoływać klasy tylko z 2. (a nie bezpośrednio wywoływać np. Funkcję bazy danych), a klasy z 2. są łatwe do wyśmiewania (ponieważ tylko ujawnij potrzebne części owiniętego systemu).

Klasy od 2. i 3. zwykle nie mogą być w znaczący sposób testowane jednostkowo (ponieważ same nie robią nic pożytecznego, są jedynie kodem „kleju”). OTOH, te klasy są zwykle stosunkowo proste (i nieliczne), więc powinny być odpowiednio objęte testami integracyjnymi.


Przykład

Jedna klasa

Załóżmy, że masz klasę, która pobiera cenę z bazy danych, stosuje pewne rabaty, a następnie aktualizuje bazę danych.

Jeśli masz to wszystko w jednej klasie, będziesz musiał wywoływać funkcje DB, które trudno wyśmiewać. W pseudokodzie:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Wszystkie trzy kroki będą wymagały dostępu do bazy danych, więc wiele (złożonych) szyderstw, które mogą ulec uszkodzeniu, jeśli zmieni się kod lub struktura bazy danych.

Rozdzielać się

Dzielisz na trzy klasy: PriceCalculation, PriceRepository, App.

PriceCalculation wykonuje tylko rzeczywiste obliczenia i otrzymuje wymagane wartości. Aplikacja wiąże wszystko razem:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

W ten sposób:

  • PriceCalculation zawiera „logikę biznesową”. Łatwo jest przetestować, ponieważ nie wywołuje niczego samodzielnie.
  • PriceRepository można przetestować pseudo-jednostkowo, konfigurując próbną bazę danych i testując wywołania odczytu i aktualizacji. Ma małą logikę, a zatem kilka ścieżek kodowych, więc nie potrzebujesz zbyt wielu z tych testów.
  • Aplikacja nie może zostać znacząco przetestowana, ponieważ jest to kod kleju. Jest to jednak bardzo proste, więc testy integracyjne powinny wystarczyć. Jeśli później aplikacja stanie się zbyt skomplikowana, przełamiesz więcej klas „logiki biznesowej”.

Wreszcie może się okazać, że PriceCalculation musi wykonywać własne wywołania bazy danych. Na przykład, ponieważ tylko PriceCalculation wie, jakie dane potrzebuje, więc aplikacja nie może ich wcześniej pobrać. Następnie możesz przekazać instancję PriceRepository (lub inną klasę repozytorium), dostosowaną do potrzeb PriceCalculation. Klasa ta będzie musiała zostać wyśmiewana, ale będzie to proste, ponieważ interfejs PriceRepository jest prosty, np PriceRepository.getPrice(articleNo, contractType). Co najważniejsze, interfejs PriceRepository izoluje PriceCalculation od bazy danych, więc zmiany schematu DB lub organizacji danych raczej nie zmienią jego interfejsu, a tym samym nie udaremnią prób.

Śleske
źródło
5
Pomyślałem, że sam nie widzę sensu w testowaniu wszystkiego przez urządzenie, dzięki
entropowie
4
Po prostu nie zgadzam się, kiedy mówisz, że klasy typu 3 są nieliczne, wydaje mi się, że większość mojego kodu jest typu 3 i prawie nie ma logiki biznesowej. To właśnie mam na myśli: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz
27

Jaka jest decydująca zaleta testów jednostkowych w porównaniu z testami integracyjnymi?

To fałszywa dychotomia.

Testy jednostkowe i integracyjne służą dwóm podobnym, ale różnym celom. Celem testów jednostkowych jest upewnienie się, że metody działają. W praktyce testy jednostkowe upewniają się, że kod spełnia warunki określone w testach jednostkowych. Jest to widoczne w sposobie projektowania testów jednostkowych: w szczególności określają, co powinien zrobić kod, i twierdzą, że kod to robi.

Testy integracyjne są różne. Testy integracyjne sprawdzają interakcję między komponentami oprogramowania. Możesz mieć komponenty oprogramowania, które przejdą wszystkie ich testy i nadal nie przejdą testów integracji, ponieważ nie działają prawidłowo.

Jeśli jednak testy jednostkowe mają decydującą przewagę, jest to następujące: testy jednostkowe są znacznie łatwiejsze do skonfigurowania i wymagają znacznie mniej czasu i wysiłku niż testy integracyjne. Przy prawidłowym stosowaniu testy jednostkowe zachęcają do opracowania kodu „testowalnego”, co oznacza, że ​​końcowy wynik będzie bardziej niezawodny, łatwiejszy do zrozumienia i łatwiejszy do utrzymania. Testowalny kod ma pewne cechy, takie jak spójny interfejs API, powtarzalne zachowanie i zwraca wyniki, które są łatwe do potwierdzenia.

Testy integracyjne są trudniejsze i droższe, ponieważ często potrzebne są skomplikowane kpiny, złożona konfiguracja i trudne stwierdzenia. Na najwyższym poziomie integracji systemu wyobraź sobie próbę symulacji interakcji człowieka w interfejsie użytkownika. Całe systemy oprogramowania są poświęcone tego rodzaju automatyzacji. I szukamy automatyzacji; testy na ludziach nie są powtarzalne i nie skalują się tak jak testy automatyczne.

Wreszcie, testy integracyjne nie gwarantują pokrycia kodu. Ile kombinacji pętli kodu, warunków i gałęzi testujesz w testach integracyjnych? Czy naprawdę wiesz Istnieją narzędzia, których można użyć w testach jednostkowych i testowanych metodach, które podadzą, ile masz pokrycia kodu i jaka jest jego cykliczna złożoność. Ale naprawdę działają dobrze tylko na poziomie metody, w której przeprowadzane są testy jednostkowe.


Jeśli Twoje testy zmieniają się za każdym razem, gdy dokonujesz refaktoryzacji, jest to inny problem. Testy jednostkowe powinny polegać na udokumentowaniu tego, co robi twoje oprogramowanie, udowodnieniu, że to robi, a następnie udowodnieniu, że robi to ponownie, gdy refaktoryzujesz podstawową implementację. Jeśli twój interfejs API ulegnie zmianie lub potrzebujesz metod, które zmienią się zgodnie ze zmianą w projekcie systemu, tak właśnie powinno się stać. Jeśli to się często zdarza, najpierw napisz testy, zanim napiszesz kod. Zmusi cię to do zastanowienia się nad ogólną architekturą i pozwoli ci pisać kod z już ustanowionym API.

Jeśli spędzasz dużo czasu, pisząc testy jednostkowe dla takiego trywialnego kodu

public string SomeProperty { get; set; }

powinieneś ponownie przeanalizować swoje podejście. Testowanie jednostkowe powinno sprawdzać zachowanie, aw powyższym wierszu kodu nie ma żadnego zachowania. Jednak stworzyłeś gdzieś zależność w swoim kodzie, ponieważ ta właściwość prawie na pewno będzie przywoływana gdzie indziej w twoim kodzie. Zamiast tego, zastanów się nad napisaniem metod, które akceptują potrzebną właściwość jako parametr:

public string SomeMethod(string someProperty);

Teraz twoja metoda nie ma żadnych zależności od czegoś poza sobą i jest teraz bardziej testowalna, ponieważ jest całkowicie samodzielna. To prawda, że ​​nie zawsze będziesz w stanie to zrobić, ale przesuwa twój kod w kierunku większej możliwości testowania i tym razem piszesz test jednostkowy dla rzeczywistego zachowania.

Robert Harvey
źródło
2
Wiem, że testy jednostkowe i testy integracyjne serwera mają różne cele, jednak nadal nie rozumiem, w jaki sposób testy jednostkowe są użyteczne, jeśli wyciszysz i wyśmiejesz wszystkie wywołania publiczne wykonywane przez testy jednostkowe. Zrozumiałbym, że „kod spełnia warunki określone w testach jednostkowych”, gdyby nie kody pośredniczące i próbne; moje testy jednostkowe są dosłownie odzwierciedleniem logiki metod, które testuję. Ty (ja) tak naprawdę niczego nie testujesz, tylko patrzysz na swój kod i „konwertujesz” go na testy. Jeśli chodzi o trudności z automatyzacją i pokryciem kodu, obecnie pracuję w Railsach i oba są dobrze zadbane.
entropowie
2
Jeśli twoje testy są tylko odzwierciedleniem logiki metody, robisz to źle. Testy jednostkowe powinny zasadniczo przekazywać metodzie wartość, akceptować wartość zwracaną i stwierdzać, jaka powinna być ta wartość zwracana. Do tego nie jest wymagana logika.
Robert Harvey
2
Ma to sens, ale nadal musi skasować wszystkie inne wywołania publiczne (db, niektóre 'globals', takie jak aktualny status użytkownika itp.) I skończyć testowaniem kodu zgodnie z logiką metody.
entropowie
1
Sądzę więc, że testy jednostkowe są w większości izolowane, co potwierdza rodzaj „zestawu danych wejściowych -> zestawu oczekiwanych wyników”?
entropowie
1
Moje doświadczenie w tworzeniu wielu testów jednostkowych i integracyjnych (nie wspominając o zaawansowanych kpinach, testach integracyjnych i narzędziach pokrycia kodu używanych w tych testach) przeczy większości twierdzeń tutaj: 1) „Celem testów jednostkowych jest upewnienie się, że twoje kod robi to, co powinien ": to samo dotyczy testowania integracji (tym bardziej); 2) „testy jednostkowe są znacznie łatwiejsze do skonfigurowania”: nie, nie są (dość często, testy integracyjne są łatwiejsze); 3) „Prawidłowe użycie, testy jednostkowe zachęcają do opracowania„ testowalnego ”kodu: to samo z testami integracyjnymi; (ciąg dalszy)
Rogério
4

Testy jednostkowe z próbami mają upewnić się, że implementacja klasy jest poprawna. Kpisz z publicznych interfejsów testowanych zależności kodu. W ten sposób masz kontrolę nad wszystkim, co jest poza klasą i masz pewność, że test zakończony niepowodzeniem jest spowodowany czymś wewnętrznym dla klasy, a nie jednym z innych obiektów.

Testujesz także zachowanie testowanej klasy, a nie implementację. W przypadku zmiany kodu (tworzenie nowych metod wewnętrznych itp.) Testy jednostkowe nie powinny zakończyć się niepowodzeniem. Ale jeśli zmieniasz to, co robi metoda publiczna, absolutnie testy powinny zakończyć się niepowodzeniem, ponieważ zmieniłeś zachowanie.

Brzmi również tak, jakbyś pisał testy po napisaniu kodu, spróbuj zamiast tego napisać pierwsze testy. Spróbuj opisać zachowanie, które powinna mieć klasa, a następnie napisz minimalną ilość kodu, aby testy przebiegły pomyślnie.

Testy jednostkowe i integracyjne są przydatne do zapewnienia jakości kodu. Testy jednostkowe badają każdy element oddzielnie. Testy integracyjne upewniają się, że wszystkie komponenty działają poprawnie. Chcę mieć oba typy w moim pakiecie testowym.

Testy jednostkowe pomogły mi w rozwoju, ponieważ mogę skupić się na jednej aplikacji na raz. Kpi z komponentów, których jeszcze nie stworzyłem. Świetnie nadają się również do regresji, ponieważ dokumentują wszelkie błędy w logice, które znalazłem (nawet w testach jednostkowych).

AKTUALIZACJA

Utworzenie testu, który tylko upewnia się, że metody są wywoływane, ma wartość, ponieważ upewniasz się, że metody faktycznie zostaną wywołane. W szczególności, jeśli najpierw piszesz testy, masz listę kontrolną metod, które muszą się zdarzyć. Ponieważ ten kod jest w dużej mierze proceduralny, nie musisz wiele sprawdzać poza tym, że wywoływane są metody. Chronisz kod przed zmianami w przyszłości. Kiedy musisz wywołać jedną metodę przed drugą. Lub że metoda jest zawsze wywoływana, nawet jeśli metoda początkowa zgłasza wyjątek.

Test dla tej metody może się nigdy nie zmieniać lub może się zmieniać tylko podczas zmiany metod. Dlaczego to jest złe? Pomaga wzmocnić za pomocą testów. Jeśli musisz zmienić test po zmianie kodu, będziesz miał zwyczaj zmieniać testy za pomocą kodu.

Schleis
źródło
A jeśli metoda nie nazywa niczego prywatnego, to nie ma sensu testować go, prawda?
entropowie
Jeśli metoda jest prywatna, nie testuje się jej jawnie, należy ją przetestować za pomocą interfejsu publicznego. Wszystkie metody publiczne powinny zostać przetestowane, aby upewnić się, że zachowanie jest prawidłowe.
Schleis
Nie, mam na myśli to, że jeśli metoda publiczna nie nazywa niczego prywatnego, czy ma sens testowanie tej metody publicznej?
entropowie
Tak. Metoda robi coś, prawda? Więc to powinno zostać przetestowane. Z punktu widzenia testowania nie wiem, czy używa czegoś prywatnego. Wiem tylko, że jeśli podam dane wejściowe A, powinienem otrzymać dane wyjściowe B.
Schleis
O tak, metoda coś robi, a to coś wywołuje inne metody publiczne (i tylko to). Tak więc sposób „poprawnego” przetestowania, polegający na zerwaniu połączeń z pewnymi wartościami zwracanymi, a następnie skonfigurowaniu oczekiwań wiadomości. Co dokładnie testujesz w tym przypadku? Czy wykonano właściwe połączenia? Cóż, napisałeś tę metodę i możesz na nią spojrzeć i zobaczyć dokładnie, co ona robi. Myślę, że testowanie jednostkowe jest bardziej odpowiednie dla izolowanych metod, które powinny być używane, takie jak „wejście -> wyjście”, więc możesz skonfigurować kilka przykładów, a następnie przeprowadzić test regresji po refaktoryzacji.
entropowie
3

Miałem podobne pytanie - dopóki nie odkryłem mocy testów komponentów. Krótko mówiąc, są one takie same jak testy jednostkowe, z tym wyjątkiem, że domyślnie nie kpisz, ale używasz prawdziwych obiektów (najlepiej poprzez wstrzykiwanie zależności).

W ten sposób możesz szybko tworzyć solidne testy z dobrym pokryciem kodu. Nie musisz ciągle aktualizować swoich makiet. Może to być nieco mniej precyzyjne niż testy jednostkowe ze 100% próbami, ale zaoszczędzony czas i pieniądze to rekompensują. Jedyne, czego naprawdę potrzebujesz, aby użyć makiet lub urządzeń, to zaplecze pamięci lub usługi zewnętrzne.

W rzeczywistości nadmierne drwiny są anty-wzorcem: anty-wzorce TDD i makiety są złe .

lastzero
źródło
0

Chociaż operacja już zaznaczyła odpowiedź, po prostu dodaję tutaj moje 2 centy.

Jaka jest decydująca zaleta testów jednostkowych w porównaniu z testami integracyjnymi (wyłączając czasochłonność)?

A także w odpowiedzi na

Kiedy wykonuję testy jednostkowe w „właściwy” sposób, tj. Przerywając każde publiczne połączenie i zwracając ustawione wartości lub kpiny, mam wrażenie, że tak naprawdę niczego nie testuję.

Jest pomocne, ale nie dokładnie to, o co poprosił OP:

Testy jednostkowe działają, ale nadal występują błędy?

z mojego małego doświadczenia w testowaniu pakietów rozumiem, że testy jednostkowe mają zawsze sprawdzać najbardziej podstawową funkcjonalność klasy na poziomie metody. Moim zdaniem każda metoda publiczna, prywatna lub wewnętrzna zasługuje na dedykowane testy jednostkowe. Nawet w moim ostatnim doświadczeniu miałem publiczną metodę, która nazywała inną małą prywatną metodą. Były więc dwa podejścia:

  1. nie twórz testów jednostkowych dla metody prywatnej.
  2. utwórz test jednostkowy dla metody prywatnej.

Jeśli myślisz logicznie, sens posiadania prywatnej metody jest taki: główna metoda publiczna staje się zbyt duża lub bałagan. Aby rozwiązać ten problem, mądrze dokonujesz refaktoryzacji i tworzysz małe fragmenty kodu, które zasługują na osobne metody prywatne, co z kolei sprawia, że ​​Twoja główna metoda publiczna jest mniej obszerna. Refaktoryzujesz, pamiętając, że ta prywatna metoda może być później ponownie wykorzystana. Mogą istnieć przypadki, w których nie ma innej metody publicznej w zależności od tej metody prywatnej, ale kto wie o przyszłości.


Biorąc pod uwagę przypadek, gdy metoda prywatna jest ponownie wykorzystywana przez wiele innych metod publicznych.

Tak więc, gdybym wybrał podejście 1: powieliłbym testy jednostkowe i byłyby one skomplikowane, ponieważ miałeś liczbę testów jednostkowych dla każdej gałęzi metody publicznej, a także prywatnej.

Gdybym wybrał podejście 2: kod napisany dla testów jednostkowych byłby stosunkowo mniejszy, a testowanie byłoby znacznie łatwiejsze.


Biorąc pod uwagę przypadek, gdy metoda prywatna nie jest ponownie wykorzystywana Nie ma sensu pisać osobnego testu jednostkowego dla tej metody.

Jeśli chodzi o testy integracyjne, są one zazwyczaj wyczerpujące i na wysokim poziomie. Powiedzą Ci, że biorąc pod uwagę wkład, wszystkie Twoje klasy powinny dojść do tego końcowego wniosku. Aby dowiedzieć się więcej na temat przydatności testów integracyjnych, zobacz wspomniany link.

shankbond
źródło