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ą?
źródło
Odpowiedzi:
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.
TDD opracuje testy. Wdrożenie sprawdzania poprawności parametrów sprawi, że przejdą.
źródło
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.
źródło
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.
źródło
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.
źródło
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:
Asercje są opcjonalne, więc jeśli ich nie lubisz, po prostu uruchom system z wyłączonymi asercjami.
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ą.)
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.
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?
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.
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.
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.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 warunekx == null
jest zawsze fałszywy. To może być bardzo przydatne.Powyższe było streszczeniem postu z mojego bloga, 21.09.2014 „Asercje i testowanie”
źródło
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.
źródło
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 .
źródło
Myślę, że interpretuję uwagi twojego kolegi inaczej niż większość pozostałych odpowiedzi.
Wydaje mi się, że argumentem jest:
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!
źródło
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.
źródło
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.
źródło
Testy określają kontrakt twojej klasy.
W następstwie tej brak testu tworzy umowy, która zawiera nieokreśloną zachowanie . Więc kiedy przechodzą
null
doFoo::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
null
doFoo::Frobnicate(Widget widget)
.I udokumentujesz tę decyzję, dołączając
źródło
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.
źródło
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.
źródło
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:
Teraz, jeśli po prostu to zrobiłeś
*(sum) = a + b
, działałoby, ale tylko przy niektórych wejściach.a = 1
ib = 2
zrobiłbysum = 3
; jednak ponieważ rozmiar sumy jest bajtema = 100
ib = 200
powstałby zsum = 44
powodu 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.źródło
sum
wskaźnik jest pusty?).