Czy TDD czyni zbędnym programowanie obronne?

104

Dzisiaj odbyłem ciekawą dyskusję z kolegą.

Jestem programistą obronnym. Uważam, że zawsze należy przestrzegać zasady „ klasa musi mieć pewność, że jej obiekty mają prawidłowy stan podczas interakcji z nią spoza klasy ”. Powodem tej reguły jest to, że klasa nie wie, kim są jej użytkownicy, i że w przewidywalny sposób powinna ponieść porażkę, gdy wejdzie w interakcję w nielegalny sposób. Moim zdaniem zasada ta dotyczy wszystkich klas.

W szczególnej sytuacji, w której dzisiaj miałem dyskusję, napisałem kod, który potwierdza, że ​​argumenty mojego konstruktora są poprawne (np. Parametr liczby całkowitej musi wynosić> 0), a jeśli warunek wstępny nie jest spełniony, wówczas generowany jest wyjątek. Z drugiej strony mój kolega uważa, że ​​taka kontrola jest zbędna, ponieważ testy jednostkowe powinny wychwycić wszelkie nieprawidłowe użycie klasy. Ponadto uważa, że ​​walidacje programowania defensywnego powinny być również testowane jednostkowo, więc programowanie defensywne wymaga dużo pracy i dlatego nie jest optymalne dla TDD.

Czy to prawda, że ​​TDD jest w stanie zastąpić programowanie obronne? Czy w związku z tym sprawdzanie poprawności parametrów (i nie mam na myśli wprowadzania danych przez użytkownika) nie jest konieczne? Czy te dwie techniki się uzupełniają?

użytkownik2180613
źródło
120
Oddajesz w pełni przetestowaną jednostkę bibliotekę bez sprawdzania konstruktora klientowi do użycia, a on łamie umowę klasy. Po co ci te testy jednostkowe?
Robert Harvey
42
IMO jest na odwrót. Programowanie obronne, odpowiednie warunki wstępne i pro oraz bogaty system typów sprawiają, że testy są zbędne.
ogrodnik
37
Czy mogę zamieścić odpowiedź, która mówi tylko „Dobry smutek?” Programowanie defensywne chroni system w czasie wykonywania. Testy sprawdzają wszystkie potencjalne warunki środowiska uruchomieniowego, o których tester może pomyśleć, w tym niepoprawne argumenty przekazane konstruktorom i innym metodom. Testy, jeśli zostaną zakończone, potwierdzą, że zachowanie środowiska uruchomieniowego będzie zgodne z oczekiwaniami, w tym zgłoszone zostaną odpowiednie wyjątki lub inne zamierzone zachowanie mające miejsce po przekazaniu nieprawidłowych argumentów. Ale testy nie robią nic cholernego, aby chronić system w czasie wykonywania.
Craig
16
„testy jednostkowe powinny wychwycić wszelkie nieprawidłowe zastosowania klasy” - uh, w jaki sposób? Testy jednostkowe wykażą zachowanie z prawidłowymi argumentami, a gdy otrzymają nieprawidłowe argumenty; nie mogą pokazać ci wszystkich argumentów, jakie kiedykolwiek zostaną podane.
OJFord
34
Nie sądzę, że widziałem lepszy przykład tego, jak dogmatyczne myślenie o tworzeniu oprogramowania może prowadzić do szkodliwych wniosków.
sdenham

Odpowiedzi:

196

To niedorzeczne. TDD zmusza kod do pomyślnego przejścia testów i zmusza cały kod do przeprowadzenia testów. Nie zapobiega to niepoprawnemu wywoływaniu kodu przez konsumentów, ani w magiczny sposób uniemożliwia programistom pominięcia przypadków testowych.

Żadna metodologia nie może zmusić użytkowników do prawidłowego używania kodu.

Jest to niewielki argumentem mają być wykonane doskonale, że jeśli nie TDD byś złapał czek w przypadku testowego> 0, przed wdrożeniem go i zająć to - prawdopodobnie przez Państwo dodanie czek. Ale jeśli zrobiłeś TDD, twoje wymaganie (> 0 w konstruktorze) najpierw pojawiłoby się jako przypadek testowy, który zawodzi. W ten sposób dajesz test po dodaniu czeku.

Rozsądne jest również przetestowanie niektórych warunków obronnych (dodałeś logikę, dlaczego nie chcesz testować czegoś tak łatwo testowalnego?). Nie jestem pewien, dlaczego nie zgadzasz się z tym.

Czy te dwie techniki się uzupełniają?

TDD opracuje testy. Wdrożenie sprawdzania poprawności parametrów sprawi, że przejdą.

kraina krańca
źródło
7
Nie zgadzam się z przekonaniem, że walidacja warunków wstępnych powinna zostać przetestowana, ale nie zgadzam się z opinią mojego kolegi, że dodatkowa praca spowodowana potrzebą przetestowania walidacji warunków wstępnych jest argumentem, aby nie tworzyć walidacji warunków wstępnych w pierwszej kolejności miejsce. Zredagowałem swój post, aby wyjaśnić.
user2180613,
20
@ user2180613 Utwórz test, który sprawdza, czy awaria warunku wstępnego jest odpowiednio obsługiwana: teraz dodanie czeku nie jest „dodatkową” pracą, jest pracą wymaganą przez TDD, aby test był zielony. Jeśli twój kolega uważa, że ​​powinieneś wykonać test, zaobserwować, że się nie udaje, a następnie i dopiero wtedy wdrożyć kontrolę warunków wstępnych, może mieć rację z punktu widzenia purystycznego TDD. Jeśli mówi, żeby całkowicie zignorować czek, to jest głupi. W TDD nie ma nic, co mówi, że nie możesz być proaktywny w pisaniu testów dla potencjalnych trybów awarii.
RM
4
@RM Nie piszesz testu w celu przetestowania warunku wstępnego. Piszesz test, aby sprawdzić oczekiwane poprawne zachowanie wywoływanego kodu. Kontrole warunków wstępnych są z punktu widzenia testu nieprzejrzystym szczegółem implementacji, który zapewnia prawidłowe zachowanie. Jeśli myślisz o lepszym sposobie zapewnienia prawidłowego stanu w wywoływanym kodzie, zrób to w ten sposób zamiast tradycyjnego sprawdzania warunków wstępnych. Test poniesie z tego, czy były udane, a nadal nie będzie wiedział, czy obchodzi jak to zrobiłeś.
Craig
@ user2180613 To niesamowite uzasadnienie: D jeśli Twoim celem w pisaniu oprogramowania jest zmniejszenie liczby testów, których potrzebujesz do napisania i uruchomienia, nie pisz żadnego oprogramowania - zero testów!
Gusdor,
3
To ostatnie zdanie tej odpowiedzi.
Robert Grant,
32

Programowanie defensywne i testy jednostkowe to dwa różne sposoby wychwytywania błędów, a każdy z nich ma inną siłę. Używanie tylko jednego sposobu wykrywania błędów powoduje, że mechanizmy wykrywania błędów są kruche. Używanie obu spowoduje wychwycenie błędów, które mogły zostać pominięte przez jedno lub drugie, nawet w kodzie, który nie jest publicznym interfejsem API; na przykład ktoś mógł zapomnieć o dodaniu testu jednostkowego nieprawidłowych danych przekazanych do publicznego interfejsu API. Sprawdzanie wszystkiego w odpowiednich miejscach oznacza większe szanse na wykrycie błędu.

W bezpieczeństwie informacji nazywa się to Defence In Depth. Posiadanie wielu warstw obrony zapewnia, że ​​jeśli jedna zawiedzie, będą jeszcze inne, aby ją złapać.

Twój kolega ma rację co do jednej rzeczy: powinieneś przetestować swoje walidacje, ale nie jest to „niepotrzebna praca”. Jest to to samo, co testowanie dowolnego innego kodu. Chcesz mieć pewność, że wszystkie zastosowania, nawet nieprawidłowe, przyniosą oczekiwany rezultat.

Opłata Kevin
źródło
Czy słusznie jest powiedzieć, że sprawdzanie poprawności parametrów jest formą sprawdzania poprawności warunków wstępnych, a testy jednostkowe to sprawdzanie poprawności warunków uzupełniających, dlatego się uzupełniają?
user2180613,
1
„Jest to to samo, co testowanie dowolnego innego kodu. Chcesz mieć pewność, że wszystkie zastosowania, nawet nieprawidłowe, przyniosą oczekiwany rezultat”. To. Żaden kod nigdy nie powinien po prostu przejść, gdy przekazane dane wejściowe nie zostały zaprojektowane do obsługi. Narusza to zasadę „szybkiego działania” i może sprawić, że debugowanie stanie się koszmarem.
jpmc26
@ user2180613 - nie do końca, ale bardziej, że testy jednostkowe sprawdzają warunki awarii, których oczekuje programista, podczas gdy techniki programowania obronnego sprawdzają warunki, których programista się nie spodziewa. Testy jednostkowe można wykorzystać do sprawdzenia poprawności warunków wstępnych (za pomocą próbnego obiektu wstrzykniętego do osoby dzwoniącej, która sprawdza warunek wstępny).
Periata Breatta
1
@ jpmc26 Tak, niepowodzenie to „oczekiwany wynik” testu. Testujesz, aby pokazać, że się nie udaje, zamiast cicho wykazywać pewne nieokreślone (nieoczekiwane) zachowanie.
KRyan
6
TDD wyłapuje błędy we własnym kodzie, programowanie defensywne wyłapuje błędy w kodzie innych osób. TDD może więc pomóc w zapewnieniu wystarczającej obrony :)
jwenting
30

TDD absolutnie nie zastępuje programowania obronnego. Zamiast tego możesz użyć TDD, aby upewnić się, że wszystkie zabezpieczenia są na miejscu i działają zgodnie z oczekiwaniami.

W TDD nie należy pisać kodu bez uprzedniego napisania testu - zgodnie z religijnym cyklem refaktora czerwono-zielonego. Oznacza to, że jeśli chcesz dodać sprawdzanie poprawności, najpierw napisz test, który wymaga tego sprawdzania poprawności. Wywołaj daną metodę z liczbami ujemnymi i zerem i spodziewaj się, że zgłosi wyjątek.

Nie zapomnij również o kroku „refaktoryzacji”. Chociaż TDD jest sterowane testowo , nie oznacza to tylko testu . Nadal powinieneś stosować odpowiedni projekt i pisać rozsądny kod. Pisanie kodu obronnego jest rozsądnym kodem, ponieważ czyni oczekiwania bardziej wyraźnymi, a Twój kod ogólnie bardziej niezawodny - wczesne wykrywanie ewentualnych błędów ułatwia ich debugowanie.

Ale czy nie powinniśmy używać testów do lokalizowania błędów? Twierdzenia i testy uzupełniają się. Dobra strategia testowa łączy różne podejścia, aby upewnić się, że oprogramowanie jest niezawodne. Tylko testy jednostkowe lub tylko testy integracyjne lub tylko stwierdzenia w kodzie są niezadowalające, potrzebujesz dobrej kombinacji, aby osiągnąć wystarczający poziom zaufania do oprogramowania przy akceptowalnym wysiłku.

Potem jest bardzo duże nieporozumienie koncepcyjne twojego współpracownika: testy jednostkowe nigdy nie mogą przetestować zastosowań twojej klasy, tylko że sama klasa działa zgodnie z oczekiwaniami w izolacji. Użyłbyś testów integracji, aby sprawdzić, czy interakcja między różnymi komponentami działa, ale kombinatoryczna eksplozja możliwych przypadków testowych uniemożliwia przetestowanie wszystkiego. Testy integracyjne powinny zatem ograniczyć się do kilku ważnych przypadków. Bardziej szczegółowe testy, które obejmują również przypadki krawędzi i przypadki błędów, są bardziej odpowiednie do testów jednostkowych.

amon
źródło
16

Testy mają na celu wsparcie i zapewnienie programowania obronnego

Programowanie defensywne chroni integralność systemu w czasie wykonywania.

Testy to (głównie statyczne) narzędzia diagnostyczne. Nigdzie nie widać twoich testów. Są jak rusztowania używane do wznoszenia wysokiego ceglanego muru lub kamiennej kopuły. Nie pozostawiasz ważnych elementów poza konstrukcją, ponieważ masz rusztowanie, które utrzymuje je podczas budowy. Masz rusztowanie podtrzymujące go podczas budowy do ułatwić umieszczenie wszystkich ważnych elementów.

EDYCJA: analogia

Co z analogią do komentarzy w kodzie?

Komentarze mają swój cel, ale mogą być zbędne, a nawet szkodliwe. Na przykład, jeśli w komentarzach umieścisz wewnętrzną wiedzę na temat kodu , a następnie zmienisz kod, komentarze w najlepszym wypadku będą nieistotne, aw najgorszym szkodliwe.

Powiedzmy, że w testach włożyłeś dużo wewnętrznej wiedzy o podstawie kodu, na przykład Metoda A nie może przyjmować wartości zerowej i argumentem MethodB musi być > 0. Następnie kod się zmienia. Null jest teraz w porządku dla A, a B może przyjmować wartości tak małe jak -10. Istniejące testy są teraz funkcjonalnie niepoprawne, ale nadal będą zaliczane.

Tak, powinieneś aktualizować testy w tym samym czasie, gdy aktualizujesz kod. Powinieneś także aktualizować (lub usuwać) komentarze w tym samym czasie, gdy aktualizujesz kod. Ale wszyscy wiemy, że te rzeczy nie zawsze się zdarzają i że popełniane są błędy.

Testy weryfikują zachowanie systemu. Że rzeczywiste zachowanie jest nieodłączną częścią samego systemu, a nie dla testów.

Co może pójść nie tak?

Celem w odniesieniu do testów jest wymyślenie wszystkiego, co może pójść nie tak, napisanie testu sprawdzającego prawidłowe zachowanie, a następnie opracowanie kodu środowiska wykonawczego, aby przejść wszystkie testy.

Co oznacza, że ​​chodzi o programowanie obronne .

Napędy TDD programowanie obronne, jeśli testy są kompleksowe.

Więcej testów, prowadzenie bardziej defensywnego programowania

Kiedy nieuchronnie zostaną znalezione błędy, pisanych jest więcej testów w celu modelowania warunków, w których występuje błąd. Następnie kod jest stała, z kodem do dokonania tych testy przechodzą, a nowe badania pozostają w zestawu testowego.

Dobry zestaw testów przekazuje zarówno dobre, jak i złe argumenty do funkcji / metody i oczekuje spójnych wyników. To z kolei oznacza, że ​​testowany komponent użyje kontroli warunków wstępnych (programowanie obronne), aby potwierdzić przekazane mu argumenty.

Ogólnie mówiąc ...

Na przykład, jeśli argument zerowy dla określonej procedury jest niepoprawny, to co najmniej jeden test przejdzie zero i będzie oczekiwać pewnego rodzaju wyjątku / błędu „nieprawidłowy argument zerowy”.

Co najmniej jeden inny test zamierza przekazać ważne oczywiście argument - lub zapętli dużą tablicę i przekaże wiele poprawnych argumentów - i potwierdzi, że wynikowy stan jest odpowiedni.

Jeśli test nie przejdzie tego argumentu zerowego i zostanie spoliczkowany oczekiwanym wyjątkiem (a ten wyjątek został zgłoszony, ponieważ kod obronnie sprawdził przekazany mu stan), wówczas wartość NULL może zostać przypisana do właściwości klasy lub zakopana w jakiejś kolekcji, w której nie powinno być.

Może to spowodować nieoczekiwane zachowanie w zupełnie innej części systemu, do której przekazywana jest instancja klasy, w odległych lokalizacjach geograficznych po dostarczeniu oprogramowania . I tego właśnie staramy się uniknąć, prawda?

Może być nawet gorzej. Instancja klasy ze stanem niepoprawnym może zostać zserializowana i zapisana w pamięci, tylko w celu spowodowania awarii, gdy zostanie odtworzona w celu późniejszego wykorzystania. Rany, nie wiem, może to jakiś mechaniczny system sterowania, który nie może się zrestartować po wyłączeniu, ponieważ nie może deserializować własnego stanu konfiguracji trwałej. Lub instancja klasy może zostać przekształcona do postaci szeregowej i przekazana do zupełnie innego systemu utworzonego przez inną jednostkę, a ten system może ulec awarii.

Zwłaszcza jeśli programiści tego innego systemu nie kodowali defensywnie.

Craig
źródło
2
To zabawne, głosowanie przychodziło tak szybko, że jest absolutnie teraz sposób, w jaki downvoter mógł przeczytać poza pierwszym akapitem.
Craig
1
:-) Właśnie głosowałem bez czytania poza pierwszym akapitem, więc mam nadzieję, że to zrównoważy ...
SusanW
1
Wydawało się, że przynajmniej mogę zrobić :-) (faktycznie, ja nie przeczytać resztę tylko upewnić Nie może być niechlujny -.! Zwłaszcza w temacie jak ten)
SusanW
1
Pomyślałem, że prawdopodobnie. :)
Craig
kontrole obronne można przeprowadzać w czasie kompilacji za pomocą narzędzi takich jak Kontrakty kodowe.
Matthew Whited,
9

Zamiast TDD porozmawiajmy ogólnie o „testowaniu oprogramowania”, a zamiast ogólnie o „programowaniu defensywnym”, porozmawiajmy o moim ulubionym sposobie programowania defensywnego, jakim jest stosowanie asercji.


Ponieważ wykonujemy testy oprogramowania, powinniśmy przestać umieszczać instrukcje aser w kodzie produkcyjnym, prawda? Pozwól mi policzyć, w jaki sposób jest to błędne:

  1. Asercje są opcjonalne, więc jeśli ich nie lubisz, po prostu uruchom system z wyłączonymi asercjami.

  2. Asercje sprawdzają rzeczy, których testowanie nie może (i nie powinno). Ponieważ testy powinny mieć widok czarnej skrzynki twojego systemu, podczas gdy asercje mają widok białej skrzynki. (Oczywiście, skoro w nim mieszkają.)

  3. Asercje są doskonałym narzędziem do dokumentacji. Żaden komentarz nigdy nie był ani nie będzie tak jednoznaczny jak fragment kodu potwierdzający to samo. Ponadto dokumentacja staje się przestarzała w miarę ewolucji kodu i nie jest w żaden sposób możliwa do wyegzekwowania przez kompilator.

  4. Asercje mogą wychwytywać błędy w kodzie testowym. Czy kiedykolwiek zdarzyło Ci się, że test kończy się niepowodzeniem i nie wiesz, kto się myli - kod produkcyjny lub test?

  5. Twierdzenia mogą być bardziej trafne niż testowanie. Testy sprawdzą, czy są określone wymagania funkcjonalne, ale kod często musi przyjmować pewne założenia, które są o wiele bardziej techniczne niż to. Ludzie, którzy piszą dokumenty wymagań funkcjonalnych, rzadko myślą o podziale na zero.

  6. Twierdzenia wskazują błędy, na które testowanie jedynie ogólnie wskazuje. W związku z tym test ustanawia obszerne warunki wstępne, wywołuje długi fragment kodu, gromadzi wyniki i stwierdza, że ​​nie są one zgodne z oczekiwaniami. Biorąc pod uwagę wystarczające rozwiązywanie problemów, ostatecznie znajdziesz dokładnie to, co poszło nie tak, ale twierdzenia zwykle znajdują to pierwsze.

  7. Asercje zmniejszają złożoność programu. Każdy pojedynczy wiersz kodu, który piszesz, zwiększa złożoność programu. Asercje i słowo kluczowe final( readonly) to jedyne znane mi konstrukcje, które faktycznie zmniejszają złożoność programu. To bezcenne.

  8. Asercje pomagają kompilatorowi lepiej zrozumieć Twój kod. Spróbuj tego w domu: void foo( Object x ) { assert x != null; if( x == null ) { } }kompilator powinien wydać ostrzeżenie informujące, że warunek x == nulljest zawsze fałszywy. To może być bardzo przydatne.

Powyższe było streszczeniem postu z mojego bloga, 21.09.2014 „Asercje i testowanie”

Mike Nakis
źródło
Myślę, że w większości nie zgadzam się z tą odpowiedzią. (5) W TDD zestawem testowym jest specyfikacja. Powinieneś napisać najprostszy kod, który sprawi, że testy przejdą pomyślnie, nic więcej. (4) Czerwono-zielony przepływ pracy gwarantuje, że test nie powiedzie się, kiedy powinien i przejdzie, gdy obecna jest zamierzona funkcjonalność. Asercje niewiele tu pomagają. (3,7) Dokumentacja jest dokumentacją, twierdzenia nie są. Ale dzięki wyraźnym założeniom kod staje się bardziej samodokumentujący. Myślałem o nich jako o wykonywalnych komentarzach. (2) Testowanie białych skrzynek może być częścią ważnej strategii testowej.
amon
5
„W TDD zestaw testowy jest specyfikacją. Powinieneś napisać najprostszy kod, który sprawi, że testy przejdą pomyślnie, nic więcej.”: Nie sądzę, że to zawsze jest dobry pomysł: jak wskazano w odpowiedzi, są dodatkowe wewnętrzne założenie w kodzie, które można chcieć zweryfikować. Co z wewnętrznymi błędami, które się wzajemnie znoszą? Testy przeszły pomyślnie, ale kilka założeń w kodzie jest błędnych, co może później prowadzić do podstępnych błędów.
Giorgio
5

Uważam, że w większości odpowiedzi brakuje krytycznego rozróżnienia: Zależy to od sposobu użycia kodu.

Czy dany moduł będzie używany przez innych klientów niezależnie od testowanej aplikacji? Jeśli udostępniasz bibliotekę lub interfejs API do użytku przez osoby trzecie, nie możesz zagwarantować, że będą one wywoływać Twój kod tylko przy użyciu poprawnych danych wejściowych. Musisz zweryfikować wszystkie dane wejściowe.

Ale jeśli dany moduł jest używany tylko przez kod, który kontrolujesz, twój przyjaciel może mieć rację. Za pomocą testów jednostkowych można sprawdzić, czy dany moduł jest wywoływany tylko przy użyciu poprawnych danych wejściowych. Sprawdzanie warunków wstępnych nadal może być uważane za dobrą praktykę, ale jest to kompromis: ja zaśmiecasz kod, który sprawdza, czy warunki, o których wiesz , że nigdy nie mogą wystąpić, po prostu zaciemniają przeznaczenie kodu.

Nie zgadzam się, że kontrole warunków wstępnych wymagają więcej testów jednostkowych. Jeśli zdecydujesz, że nie musisz testować niektórych nieprawidłowych danych wejściowych, nie powinno mieć znaczenia, czy funkcja zawiera sprawdzanie warunków wstępnych, czy nie. Pamiętaj, że testy powinny weryfikować zachowanie, a nie szczegóły implementacji.

JacquesB
źródło
4
Jeśli wywoływana procedura nie weryfikuje poprawności danych wejściowych (co jest pierwotną debatą), testy jednostkowe nie mogą zapewnić, że moduł zostanie wywołany tylko z poprawnymi danymi wejściowymi. W szczególności można go wywołać z nieprawidłowymi danymi wejściowymi, ale w testowanych przypadkach i tak zwraca prawidłowy wynik - istnieją różne typy niezdefiniowanych zachowań, obsługa przepełnienia itp., Które mogą zwrócić oczekiwany wynik w środowisku testowym z wyłączonymi optymalizacjami, ale awaria produkcji.
Peteris,
@Peteris: Czy myślisz o nieokreślonym zachowaniu jak w C? Wywoływanie niezdefiniowanych zachowań, które mają różne wyniki w różnych środowiskach, jest oczywiście błędem, ale nie można temu zapobiec również przez sprawdzenie warunków wstępnych. Np. Jak sprawdzić, czy argument wskaźnika wskazuje prawidłową pamięć?
JacquesB
3
Działa to tylko w najmniejszych sklepach. Gdy twój zespół przekroczy liczbę, powiedzmy, sześciu osób, i tak będziesz potrzebować sprawdzania poprawności.
Robert Harvey
1
@RobertHarvey: W takim przypadku system należy podzielić na podsystemy z dobrze zdefiniowanymi interfejsami, a sprawdzenie poprawności danych wejściowych należy przeprowadzić na interfejsie.
JacquesB
to. To zależy od kodu, czy ten kod będzie używany przez zespół? Czy zespół ma dostęp do kodu źródłowego? Jeśli jego czysto wewnętrzny kod to sprawdzanie argumentów może być tylko obciążeniem, na przykład, sprawdzasz 0, następnie rzucasz wyjątek, a osoba dzwoniąca sprawdza kod, oh ta klasa może zgłaszać wyjątek itp. I czekać ... w tym przypadku obiekt nigdy nie otrzyma 0, ponieważ są one wcześniej odfiltrowywane 2 poziomy. Jeśli to kod biblioteki, który ma być używany przez strony trzecie, to inna historia. Nie cały kod jest napisany do użytku przez cały świat.
Aleksander Fular
3

Ten argument trochę mnie zaskakuje, ponieważ kiedy zacząłem ćwiczyć TDD, moje testy jednostkowe postaci „obiekt reaguje <w pewien sposób> gdy <nieprawidłowe wejście>” wzrosły 2 lub 3 razy. Zastanawiam się, jak twój kolega udaje się pomyślnie przejść tego rodzaju testy jednostkowe bez sprawdzania poprawności przez jego funkcje.

Odwrotny przypadek, że testy jednostkowe pokazują, że nigdy nie produkujesz złych wyników, które zostaną przekazane argumentom innych funkcji, jest znacznie trudniejszy do udowodnienia. Podobnie jak w pierwszym przypadku, zależy to w dużej mierze od dokładnego pokrycia przypadków brzegowych, ale masz dodatkowy wymóg, aby wszystkie dane wejściowe funkcji musiały pochodzić z danych wyjściowych innych funkcji, których dane wyjściowe przetestowałeś, a nie od, powiedzmy, danych wejściowych użytkownika lub moduły stron trzecich.

Innymi słowy, to, co robi TDD, nie powstrzymuje Cię przed potrzebowaniem kodu weryfikacyjnego, ale pomaga uniknąć jego zapomnienia .

Karl Bielefeldt
źródło
2

Myślę, że interpretuję uwagi twojego kolegi inaczej niż większość pozostałych odpowiedzi.

Wydaje mi się, że argumentem jest:

  • Cały nasz kod jest testowany jednostkowo.
  • Cały kod, który używa twojego komponentu, jest naszym kodem, a jeśli nie, jest testowany jednostkowo przez kogoś innego (nie jest to wyraźnie określone, ale rozumiem to z „testów jednostkowych powinien wychwycić wszelkie nieprawidłowe użycie klasy”).
  • Dlatego dla każdego wywołującego twoją funkcję jest gdzieś test jednostkowy, który wyśmiewa twój komponent, a test kończy się niepowodzeniem, jeśli wywołujący przekaże niepoprawną wartość do tego próbnego elementu.
  • Dlatego nie ma znaczenia, co robi twoja funkcja po przekazaniu niepoprawnej wartości, ponieważ nasze testy mówią, że to nie może się zdarzyć.

Dla mnie ten argument ma pewną logikę, ale zbytnio polega na testach jednostkowych, aby uwzględnić każdą możliwą sytuację. Prostym faktem jest to, że 100% pokrycia linii / gałęzi / ścieżki niekoniecznie wykonuje każdą wartość, którą może przekazać rozmówca, podczas gdy 100% pokrycia wszystkich możliwych stanów dzwoniącego (to znaczy wszystkich możliwych wartości jego wejść i zmienne) jest niewykonalne obliczeniowo.

Dlatego wolałbym testować jednostki wywołujące, aby upewnić się, że (o ile testy się sprawdzają) nigdy nie przejdą złych wartości, a dodatkowo wymagać, aby twój komponent zawiódł w rozpoznawalny sposób, gdy przekazywana jest zła wartość ( przynajmniej o ile to możliwe, aby rozpoznać złe wartości w wybranym języku). Pomoże to w debugowaniu, gdy pojawią się problemy w testach integracyjnych, a także pomoże wszystkim użytkownikom z twojej klasy, którzy są mniej niż rygorystyczni w izolowaniu jednostki kodu od tej zależności.

Pamiętaj jednak, że jeśli udokumentujesz i przetestujesz zachowanie swojej funkcji, gdy zostanie przekazana wartość <= 0, wartości ujemne nie będą już nieprawidłowe (przynajmniej nie więcej niż jakikolwiek argument throw, ponieważ jest również udokumentowane, aby zgłosić wyjątek!). Dzwoniący mają prawo polegać na tym obronnym zachowaniu. Jeśli pozwala na to język, być może jest to w każdym razie najlepszy scenariusz - funkcja nie ma „niepoprawnych danych wejściowych”, ale osoby dzwoniące, które oczekują, że nie sprowokują funkcji do zgłoszenia wyjątku, powinny zostać wystarczająco przetestowane w jednostce, aby upewnić się, że nie „ przekazać wartości, które to powodują.

Mimo że uważam, że twój kolega jest nieco mniej całkowicie błędny niż większość odpowiedzi, dochodzę do tego samego wniosku, że obie techniki się uzupełniają. Programuj defensywnie, dokumentuj kontrole obronne i testuj je. Praca jest „niepotrzebna” tylko wtedy, gdy użytkownicy Twojego kodu nie mogą skorzystać z przydatnych komunikatów o błędach, gdy popełniają błędy. Teoretycznie, jeśli dokładnie przetestują cały swój kod przed zintegrowaniem go z twoim i nigdy nie będzie żadnych błędów w swoich testach, to nigdy nie zobaczą komunikatów o błędach. W praktyce, nawet jeśli robią TDD i zastrzyki z całkowitą zależnością, nadal mogą eksplorować podczas rozwoju lub mogą wystąpić przerwy w testowaniu. W rezultacie wywołują Twój kod, zanim jego kod będzie idealny!

Steve Jessop
źródło
Ta sprawa polegająca na kładzeniu nacisku na testowanie dzwoniących pod kątem upewnienia się, że nie przechodzą złych wartości, wydaje się nadawać do kruchego kodu z dużą ilością zależności od basu i bez wyraźnego oddzielania obaw. Naprawdę nie sądzę, żebym chciał kod, który powstałby w wyniku takiego podejścia.
Craig,
@Craig: patrzeć na to w ten sposób, jeśli pojedyncze komponent do testu przez przedrzeźniając jego zależności, to dlaczego ty nie przetestować, że tylko przechodzi poprawne wartości tych zależności? A jeśli nie możesz wyizolować komponentu, czy naprawdę rozdzieliłeś obawy? Nie zgadzam się z kodowaniem obronnym, ale jeśli testy obronne są środkiem, za pomocą którego testujesz poprawność wywoływania kodu, to jest bałagan. Myślę więc, że kolega pytającego ma rację, że czeki są zbędne, ale źle widzę w tym powód, aby ich nie pisać :-)
Steve Jessop
jedyną rażącą dziurą, jaką widzę, jest to, że wciąż testuję tylko to, czy moje własne komponenty nie mogą przekazać niepoprawnych wartości do tych zależności, co całkowicie zgadzam się, że powinno być zrobione, ale ile decyzji musi podjąć ilu menedżerów biznesowych, aby stworzyć prywatny komponent publiczny, aby partnerzy mogli to nazwać? W rzeczywistości przypomina mi to projektowanie baz danych i cały obecny romans z ORM, w wyniku czego tak wiele (głównie młodszych) osób deklaruje, że bazy danych są po prostu głupim miejscem do przechowywania w sieci i nie powinny się chronić przed ograniczeniami, kluczami obcymi i procedurami przechowywanymi.
Craig
Inną rzeczą, którą widzę, jest to, że w tym scenariuszu oczywiście testujesz tylko połączenia z próbami, a nie z rzeczywistymi zależnościami. Ostatecznie jest to kod w tych zależnościach, który może lub nie może odpowiednio działać z określoną przekazaną wartością, a nie kod w wywołującym. Tak więc zależność musi być właściwa i musi istnieć wystarczająca niezależna analiza testu zależności, aby się upewnić. Pamiętaj, że testy, o których mówimy, nazywane są testami „jednostkowymi”. Każda zależność jest jednostką. :)
Craig,
1

Interfejsy publiczne mogą i będą niewłaściwie wykorzystywane

Twierdzenie współpracownika „testy jednostkowe powinny wyłapać wszelkie nieprawidłowe użycie klasy” jest całkowicie fałszywe dla każdego interfejsu, który nie jest prywatny. Jeśli funkcja publiczna może być wywoływana z argumentami liczb całkowitych, wówczas może i będzie wywoływana z dowolnymi argumentami liczb całkowitych, a kod powinien zachowywać się odpowiednio. Jeśli podpis funkcji publicznej akceptuje np. Typ Java Double, wówczas wszystkie możliwe wartości to null, NaN, MAX_VALUE, -Inf. Twoje testy jednostkowe nie może złapać niepoprawnych zastosowań klasy, ponieważ badania te nie mogą przetestować kod, który będzie korzystał z tej klasy, ponieważ kod nie jest jeszcze napisane, nie może być napisany przez ciebie, a na pewno będzie poza zakresem swoich testów jednostkowych .

Z drugiej strony to podejście może być poprawne dla (prywatnych, liczących o wiele liczniejszych) właściwości prywatnych - jeśli klasa może zapewnić, że jakiś fakt zawsze jest prawdziwy (np. Właściwość X nie może być zawsze pusta, liczba całkowita nie przekracza maksymalnej długości , gdy wywoływana jest funkcja A, wszystkie wymagane struktury danych są dobrze uformowane), wówczas może być właściwe unikanie sprawdzania tego raz za razem ze względu na wydajność i zamiast tego polegać na testach jednostkowych.

Piotr jest
źródło
Nagłówek i pierwszy akapit tego jest prawdą, ponieważ to nie testy jednostkowe będą wykonywać kod w czasie wykonywania. Jest to dowolny inny kod środowiska wykonawczego i zmieniające się warunki rzeczywiste oraz złe dane wejściowe użytkownika i próby włamania się do kodu.
Craig,
1

Obrona przed niewłaściwym użyciem jest funkcją opracowaną ze względu na wymagania. (Nie wszystkie interfejsy wymagają rygorystycznych kontroli pod kątem niewłaściwego użycia; na przykład bardzo wąsko używane wewnętrzne).

Ta funkcja wymaga przetestowania: czy ochrona przed niewłaściwym użyciem rzeczywiście działa? Celem przetestowania tej funkcji jest próba wykazania, że ​​tak nie jest: spowodowanie niewłaściwego użycia modułu, który nie został złapany przez jego kontrole.

Jeśli wymagane są określone kontrole, twierdzenie, że istnienie niektórych testów czyni je zbędnymi, jest nonsensem. Jeśli jest to funkcja jakiejś funkcji, która (powiedzmy) zgłasza wyjątek, gdy parametr trzeci jest ujemny, to nie podlega negocjacji; zrobi to.

Podejrzewam jednak, że twój kolega ma sens z punktu widzenia sytuacji, w której nie ma wymogu szczegółowych kontroli danych wejściowych, z konkretnymi reakcjami na złe dane wejściowe: sytuacja, w której istnieje tylko ogólny wymóg dotyczący krzepkość.

Kontrole przy wejściu do funkcji najwyższego poziomu mają częściowo na celu ochronę słabego lub źle przetestowanego kodu wewnętrznego przed nieoczekiwanymi kombinacjami parametrów (np. Jeśli kod jest dobrze przetestowany, kontrole nie są konieczne: kod może po prostu „ pogoda ”złe parametry).

W idei kolegi jest prawda, a to, co najprawdopodobniej ma na myśli, to: jeśli zbudujemy funkcję z bardzo solidnych elementów niższego poziomu, które są defensywnie kodowane i indywidualnie testowane pod kątem wszelkiego niewłaściwego użycia, możliwe jest, aby funkcja wyższego poziomu solidne bez własnych, obszernych kontroli wewnętrznych.

Jeśli jego umowa zostanie naruszona, przełoży się to na niewłaściwe użycie funkcji niższego poziomu, być może przez wprowadzenie wyjątków lub cokolwiek innego.

Jedynym problemem jest to, że wyjątki niższego poziomu nie są specyficzne dla interfejsu wyższego poziomu. To, czy jest to problem, zależy od wymagań. Jeśli wymaganiem jest po prostu „funkcja powinna być odporna na niewłaściwe użycie i rzucić jakiś wyjątek zamiast awarii lub kontynuować obliczanie z danymi śmieciowymi”, wówczas w rzeczywistości może być objęta całą odpornością elementów niższego poziomu, na których jest wybudowany.

Jeśli funkcja ma wymaganie bardzo szczegółowych, szczegółowych raportów o błędach związanych z jej parametrami, wówczas kontrole na niższym poziomie nie spełniają w pełni tych wymagań. Zapewniają tylko, że funkcja jakoś wysadzi (nie kontynuuje z niewłaściwą kombinacją parametrów, powodując efekt śmieci). Jeśli kod klienta został napisany w celu wychwycenia określonych błędów i obsługi ich, może nie działać poprawnie. Kod klienta może sam uzyskiwać, jako dane wejściowe, dane, na których opierają się parametry, i może oczekiwać, że funkcja je sprawdzi i przełoży złe wartości na określone błędy, tak jak to udokumentowano (aby mógł je obsłużyć błędy poprawnie) zamiast niektórych innych błędów, które nie są obsługiwane i być może zatrzymują obraz oprogramowania.

TL; DR: Twój kolega prawdopodobnie nie jest idiotą; po prostu rozmawiacie obok siebie z różnymi perspektywami na tę samą rzecz, ponieważ wymagania nie są w pełni sprecyzowane i każdy z was ma inne wyobrażenie o tym, czym są „niepisane wymagania”. Uważasz, że jeśli nie ma określonych wymagań dotyczących sprawdzania parametrów, i tak powinieneś sprawdzić szczegółowe sprawdzanie; kolega myśli, pozwól solidnemu kodowi niższego poziomu wysadzić w powietrze, gdy parametry są nieprawidłowe. Kłócenie się o niepisane wymagania za pomocą kodu jest nieco bezproduktywne: pamiętaj, że nie zgadzasz się co do wymagań, a nie kodu. Twój sposób kodowania odzwierciedla, Twoim zdaniem, wymagania; sposób kolegi reprezentuje jego pogląd na wymagania. Jeśli spojrzysz na to w ten sposób, jasne jest, że to, co jest dobre, a co złe, nie jest t w samym kodzie; kod jest tylko proxy dla twojej opinii o tym, jaka powinna być specyfikacja.

Kaz
źródło
Jest to związane z ogólną filozoficzną trudnością radzenia sobie z luźnymi wymaganiami. Jeśli dana funkcja ma znaczące, ale nie całkowite, wolne panowanie, może zachowywać się arbitralnie, gdy otrzyma zniekształcone dane wejściowe (np. Jeśli dekoder obrazu może spełniać wymagania, jeśli można zagwarantować, że - w wolnym czasie - wytworzy dowolną kombinację pikseli lub zakończy się nieprawidłowo , ale nie jeśli może to pozwolić złośliwie spreparowanym wejściom na wykonanie dowolnego kodu), może nie być jasne, jakie przypadki testowe byłyby odpowiednie, aby upewnić się, że żadne dane wejściowe nie powodują niedopuszczalnego działania.
supercat
1

Testy określają kontrakt twojej klasy.

W następstwie tej brak testu tworzy umowy, która zawiera nieokreśloną zachowanie . Więc kiedy przechodzą nulldo Foo::Frobnicate(Widget widget)i run-time niewypowiedziane spustoszenie wynika, wciąż jesteś w umowie swojej klasy.

Później decydujesz: „nie chcemy możliwości nieokreślonego zachowania”, co jest rozsądnym wyborem. Oznacza to, że trzeba mieć oczekiwanej zachowanie dla przechodząc nulldo Foo::Frobnicate(Widget widget).

I udokumentujesz tę decyzję, dołączając

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
Caleth
źródło
1

Dobry zestaw testów sprawdzi zewnętrzny interfejs twojej klasy i zapewni, że takie niewłaściwe użycie wygeneruje poprawną odpowiedź (wyjątek lub cokolwiek, co określisz jako „poprawne”). W rzeczywistości pierwszym przypadkiem testowym, który piszę dla klasy, jest wywołanie jej konstruktora z argumentami spoza zakresu.

Programem defensywnym, który jest zazwyczaj eliminowany przez podejście w pełni testowane jednostkowo, jest niepotrzebne sprawdzanie wewnętrznych niezmienników, których nie można naruszyć przez zewnętrzny kod.

Przydatnym pomysłem, który czasem stosuję, jest zapewnienie metody, która testuje niezmienniki obiektu; twoja metoda usuwania może wywołać ją, aby potwierdzić, że twoje zewnętrzne działania na obiekcie nigdy nie łamią niezmienników.

Toby Speight
źródło
0

Testy TDD wychwytują błędy podczas opracowywania kodu .

Sprawdzanie granic, które opisujesz jako część programowania obronnego, będzie zawierało błędy podczas używania kodu .

Jeśli dwie domeny są takie same, to znaczy kod, który piszesz, jest zawsze używany wewnętrznie przez ten konkretny projekt, to może być prawdą, że TDD wyklucza konieczność sprawdzania granic programowania obronnego, które opisujesz, ale tylko jeśli te typy sprawdzanie granic jest przeprowadzane specjalnie w testach TDD .


Jako konkretny przykład, załóżmy, że biblioteka kodu finansowego została opracowana przy użyciu TDD. Jeden z testów może stwierdzić, że określona wartość nigdy nie może być ujemna. To gwarantuje, że twórcy biblioteki nie przypadkowo wykorzystają klasy podczas implementacji funkcji.

Ale po wydaniu biblioteki i używaniu jej we własnym programie testy TDD nie uniemożliwiają mi przypisania wartości ujemnej (zakładając, że jest ujawniona). Sprawdzanie granic.

Chodzi mi o to, że chociaż twierdzenie TDD może rozwiązać problem z wartością ujemną, jeśli kod jest kiedykolwiek używany tylko wewnętrznie jako część rozwoju większej aplikacji (w ramach TDD), to jeśli będzie to biblioteka używana przez innych programistów bez TDD ramy i testy , sprawdzanie granic.

Czarny Jastrząb
źródło
1
Nie głosowałem za głosem, ale zgadzam się z głosowaniem za założeniem, że dodanie subtelnych różnic do tego rodzaju argumentów zamazuje wodę.
Craig
@Craig Byłbym zainteresowany twoją opinią na temat konkretnego przykładu, który dodałem.
Blackhawk,
Podoba mi się specyfika tego przykładu. Jedyny problem, który wciąż mam, dotyczy całego argumentu. Na przykład; pojawia się nowy zespół w zespole i pisze nowy komponent, który korzysta z tego modułu finansowego. Nowy facet nie jest świadomy wszystkich zawiłości systemu, nie mówiąc już o tym, że wszelkiego rodzaju wiedza ekspercka na temat tego, jak ma działać system, jest osadzona w testach, a nie w testowanym kodzie.
Craig,
Tak więc nowy facet / gal pomija tworzenie niektórych testów życiowych, A TY kończysz z nadmiarowością w testach - testy w różnych częściach systemu sprawdzają te same warunki i stają się niespójne w miarę upływu czasu, zamiast po prostu stawiać odpowiednie twierdzenia i sprawdzenie warunków wstępnych w kodzie, w którym znajduje się działanie.
Craig,
1
Coś w tym stylu. Tyle, że wiele argumentów dotyczyło przeprowadzenia testów kodu wywołującego. Ale jeśli masz jakiś stopień wachlowania, w końcu robisz te same kontrole z wielu różnych miejsc, a to samo w sobie stanowi problem z utrzymaniem. Co się stanie, jeśli zmieni się zakres prawidłowych danych wejściowych dla procedury, ale masz wiedzę domenową dla tego zakresu wbudowaną w testy wykorzystujące różne składniki? Nadal jestem całkowicie zwolennikiem programowania obronnego i profilowania, aby ustalić, czy i kiedy masz problemy z wydajnością.
Craig,
0

TDD i programowanie obronne idą w parze. Korzystanie z obu nie jest zbędne, ale w rzeczywistości się uzupełnia. Kiedy masz funkcję, chcesz się upewnić, że działa ona zgodnie z opisem i napisać dla niej testy; jeśli nie omówisz, co się dzieje, gdy w przypadku złych danych wejściowych, złego powrotu, złego stanu itp. nie piszesz wystarczająco solidnie testów, a kod będzie kruchy, nawet jeśli wszystkie testy przejdą pomyślnie.

Jako inżynier wbudowany lubię przykład pisania funkcji, aby po prostu dodać dwa bajty razem i zwrócić wynik w następujący sposób:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Teraz, jeśli po prostu to zrobiłeś *(sum) = a + b, działałoby, ale tylko przy niektórych wejściach. a = 1i b = 2zrobiłby sum = 3; jednak ponieważ rozmiar sumy jest bajtem a = 100i b = 200powstałby z sum = 44powodu przepełnienia. W C zwracamy w tym przypadku błąd, co oznacza, że ​​funkcja nie powiodła się; zgłaszanie wyjątku jest tym samym w kodzie. Nieuwzględnienie awarii lub przetestowanie sposobu ich obsługi nie będzie działać długoterminowo, ponieważ jeśli wystąpią te warunki, nie zostaną one obsłużone i mogą powodować wiele problemów.

Dom
źródło
To wygląda na dobry przykład pytania do wywiadu (dlaczego ma wartość zwracaną i parametr „out” - i co się dzieje, gdy sumwskaźnik jest pusty?).
Toby Speight