Niedawno pisałem mały fragment kodu, który w przyjazny dla człowieka sposób wskazywałby, ile lat ma wydarzenie. Może to na przykład oznaczać, że wydarzenie miało miejsce „Trzy tygodnie temu”, „Miesiąc temu” lub „Wczoraj”.
Wymagania były stosunkowo jasne i był to idealny przypadek dla rozwoju opartego na testach. Testy pisałem jeden po drugim, wdrażając kod, aby przejść każdy test i wszystko wydawało się działać idealnie. Do momentu pojawienia się błędu w produkcji.
Oto odpowiedni fragment kodu:
now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
return "Today"
yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
return "Yesterday"
delta = (now - event_date).days
if delta < 7:
return _number_to_text(delta) + " days ago"
if delta < 30:
weeks = math.floor(delta / 7)
if weeks == 1:
return "A week ago"
return _number_to_text(weeks) + " weeks ago"
if delta < 365:
... # Handle months and years in similar manner.
Testy sprawdzały przypadek zdarzenia, które miało miejsce dzisiaj, wczoraj, cztery dni temu, dwa tygodnie temu, tydzień temu itp., A kod został odpowiednio zbudowany.
Tęskniłem za tym, że wydarzenie może zdarzyć się dzień przed wczoraj, a być dzień temu: na przykład wydarzenie, które wydarzyło się dwadzieścia sześć godzin temu, byłoby dzień temu, a nie dokładnie wczoraj, jeśli teraz jest 1 rano. Dokładniej, to jeden punkt coś, ale ponieważ delta
jest liczbą całkowitą, będzie tylko jedna. W takim przypadku aplikacja wyświetla „One days ago”, co jest oczywiście nieoczekiwane i nieobsługiwane w kodzie. Można to naprawić, dodając:
if delta == 1:
return "A day ago"
zaraz po obliczeniu delta
.
Chociaż jedyną negatywną konsekwencją tego błędu jest to, że zmarnowałem pół godziny zastanawiając się, jak może się zdarzyć ta sprawa (i wierząc, że ma to związek ze strefami czasowymi, pomimo jednolitego użycia UTC w kodzie), niepokoi mnie jej obecność. Wskazuje, że:
- Bardzo łatwo jest popełnić błąd logiczny nawet w tak prostym kodzie źródłowym.
- Rozwój oparty na testach nie pomógł.
Niepokojące jest również to, że nie widzę, jak można uniknąć takich błędów. Pomijając więcej myślenia przed napisaniem kodu, jedyne, co mogę wymyślić, to dodać wiele stwierdzeń dla przypadków, które moim zdaniem nigdy by się nie wydarzyły (tak jak myślałem, że dzień temu jest koniecznie wczoraj), a następnie przeglądać co sekundę w ciągu ostatnich dziesięciu lat sprawdzanie, czy nie dochodzi do naruszenia twierdzeń, które wydają się zbyt skomplikowane.
Jak mogę uniknąć tworzenia tego błędu?
źródło
Odpowiedzi:
Są to rodzaje błędów, które zwykle można znaleźć na etapie refaktoryzacji czerwonego / zielonego / refaktora. Nie zapomnij o tym kroku! Rozważmy refaktor taki jak poniżej (niesprawdzony):
Tutaj stworzyłeś 3 funkcje na niższym poziomie abstrakcji, które są znacznie bardziej spójne i łatwiejsze do przetestowania w izolacji. Jeśli pominąłeś zamierzony okres czasu, wyglądałby on jak obolały kciuk w prostszych funkcjach pomocnika. Ponadto, usuwając duplikację, zmniejszasz ryzyko błędu. Będziesz musiał dodać kod, aby zaimplementować swój zepsuty przypadek.
Inne bardziej subtelne przypadki testowe łatwiej przychodzą na myśl, gdy patrzymy na taką zreformowaną formę. Na przykład, co należy
best_unit
zrobić, jeślidelta
jest negatywne?Innymi słowy, refaktoryzacja nie służy tylko upiększaniu. Ułatwia ludziom wykrycie błędów, których nie potrafi kompilator.
źródło
pluralize
praca tylko dla podzbioru angielskich słów będzie odpowiedzialnością.pluralize
użycianum
iunit
zbudowania jakiegoś klucza, aby pobrać ciąg formatu z pliku tabeli / zasobu. LUB możesz potrzebować pełnego przepisania logiki, ponieważ potrzebujesz różnych jednostek ;-)Wygląda na to, że to pomogło, po prostu nie miałeś testu na scenariusz „dzień temu”. Prawdopodobnie dodałeś test po znalezieniu tego przypadku; nadal jest to TDD, ponieważ gdy zostaną znalezione błędy, piszesz test jednostkowy w celu wykrycia błędu, a następnie napraw go.
Jeśli zapomnisz napisać test na zachowanie, TDD nie ma ci nic do roboty; zapominasz napisać test i dlatego nie piszesz implementacji.
źródło
datetime.utcnow()
usunięcia go z funkcji, a zamiast tego przekazanianow
jako (odtwarzalnego) argumentu.Testy niewiele pomogą, jeśli problem jest źle zdefiniowany. Widocznie łączysz dni kalendarzowe z dniami liczonymi w godzinach. Jeśli trzymasz się dni kalendarzowych, to o 1 w nocy 26 godzin temu nie jest wczoraj. A jeśli trzymasz się godzin, to 26 godzin temu przechodzi do 1 dnia temu, niezależnie od godziny.
źródło
Nie możesz TDD doskonale chroni Cię przed możliwymi problemami, o których wiesz. To nie pomaga, jeśli napotkasz problemy, których nigdy nie rozważałeś. Najlepiej jest, gdy ktoś inny przetestuje system, może znaleźć przypadki, których nigdy nie wziąłeś pod uwagę.
Czytanie pokrewne: Czy możliwe jest osiągnięcie bezwzględnego stanu zerowego błędu dla oprogramowania na dużą skalę?
źródło
Są dwa podejścia, które zwykle przyjmuję, i które mogą pomóc.
Najpierw szukam skrzynek. To są miejsca, w których zachowanie się zmienia. W twoim przypadku zachowanie zmienia się w kilku punktach w ciągu dodatnich dni całkowitych. Przypadek zbocza ma wartość zero, jeden, siódmą itd. Piszę wtedy przypadki testowe na i wokół przypadków skrajnych. Miałbym przypadki testowe po -1 dniach, 0 dniach, 1 godzinie, 23 godzinach, 24 godzinach, 25 godzinach, 6 dniach, 7 dniach, 8 dniach itp.
Druga rzecz, której szukam, to wzorce zachowań. W swojej logice od tygodni masz specjalną obsługę przez jeden tydzień. Prawdopodobnie masz podobną logikę w każdym z pozostałych przedziałów, których nie pokazano. Jednak ta logika nie jest obecna przez kilka dni. Patrzyłem na to z podejrzliwością, dopóki nie będę w stanie zweryfikować, dlaczego ta sprawa jest inna, lub dodam logikę.
źródło
Państwo nie może złapać błędów logicznych, które są obecne w swoje wymagania z TDD. Ale nadal TDD pomaga. W końcu znalazłeś błąd i dodałeś przypadek testowy. Ale zasadniczo TDD zapewnia tylko, że kod jest zgodny z twoim modelem mentalnym. Jeśli twój model mentalny jest wadliwy, przypadki testowe go nie złapią.
Pamiętaj jednak, że naprawiając błąd, przypadki testowe, które już upewniłeś się, że żadne istniejące, działające zachowanie nie zostało złamane. To dość ważne, łatwo naprawić jeden błąd, ale wprowadzić inny.
Aby wcześniej znaleźć te błędy, zwykle próbujesz użyć przypadków testowych opartych na klasie równoważności. stosując tę zasadę, wybierałbyś jeden przypadek z każdej klasy równoważności, a następnie wszystkie przypadki skrajne.
Jako przykłady z każdej klasy równoważności wybrałbyś datę od dzisiaj, wczoraj, kilka dni temu, dokładnie tydzień temu i kilka tygodni temu. Podczas testowania dat należy również upewnić się, że w testach nie wykorzystano daty systemowej, ale do porównania wykorzystano wcześniej ustaloną datę. Podkreśliłoby to również niektóre przypadki brzegowe: Upewnij się, że przeprowadzasz testy o dowolnej porze dnia, uruchamiasz je bezpośrednio po północy, bezpośrednio przed północą, a nawet bezpośrednio o północy. Oznacza to, że dla każdego testu byłyby cztery podstawowe czasy, w których byłby testowany.
Następnie systematycznie dodawałeś przypadki krawędzi do wszystkich innych klas. Masz test na dzisiaj. Dodaj więc czas przed i po zachowaniu powinno się zmienić. To samo na wczoraj. To samo przez tydzień temu itp.
Szanse są takie, że poprzez systematyczne wyliczanie wszystkich przypadków skrajnych i zapisywanie dla nich przypadków testowych, dowiadujesz się, że w specyfikacji brakuje pewnych szczegółów i dodajesz ją. Zauważ, że obsługa dat jest czymś, co ludzie często mylą się, ponieważ często zapominają pisać swoje testy, aby można je było uruchamiać o różnych porach.
Zauważ jednak, że większość tego, co napisałem, ma niewiele wspólnego z TDD. Chodzi o spisanie klas równoważności i upewnienie się, że własne specyfikacje są wystarczająco szczegółowe na ich temat. Jest to proces, za pomocą którego minimalizujesz błędy logiczne. TDD tylko upewnia się, że twój kod jest zgodny z twoim modelem mentalnym.
Wymyślanie przypadków testowych jest trudne . Testy oparte na klasie równoważności to jeszcze nie wszystko, aw niektórych przypadkach może znacznie zwiększyć liczbę przypadków testowych. W prawdziwym świecie dodanie wszystkich tych testów często nie jest ekonomicznie opłacalne (chociaż teoretycznie należy to zrobić).
źródło
Dlaczego nie? Brzmi jak niezły pomysł!
Dodanie kontraktów (asercji) do kodu jest dość solidnym sposobem na poprawienie jego poprawności. Generalnie możemy dodać je jako warunków dotyczących wjazdu funkcję i postconditions na powrót funkcji. Na przykład, możemy dodać postcondition że wszystkie zwrócone wartości są zarówno formy „A [jednostka] temu” lub „[liczba] [jednostka] s temu”. W przypadku zdyscyplinowania prowadzi to do zaprojektowania na podstawie umowy i jest jednym z najczęstszych sposobów pisania kodu o wysokiej pewności.
Krytycznie, umowy nie są przeznaczone do testowania; są one w równym stopniu specyfikacjami kodu, co testy. Możesz jednak przeprowadzić test za pomocą umów: zadzwoń do kodu w teście, a jeśli żadna z umów nie spowoduje błędów, test się powiedzie. Pętla w każdej sekundzie ostatnich dziesięciu lat to trochę za dużo. Ale możemy wykorzystać inny styl testowania, zwany testowaniem opartym na właściwościach .
W PBT zamiast testowania określonych danych wyjściowych kodu testujesz, czy dane wyjściowe są zgodne z pewną właściwością. Na przykład, jedna właściwość
reverse()
funkcji jest to, że dla każdej listyl
,reverse(reverse(l)) = l
. Zaletą pisania takich testów jest to, że silnik PBT może wygenerować kilkaset dowolnych list (i kilka patologicznych) i sprawdzić, czy wszystkie mają tę właściwość. Jeśli nie , silnik „kurczy się” w przypadku niepowodzenia, aby znaleźć minimalną listę, która łamie kod. Wygląda na to, że piszesz Python, który ma Hipotezę jako główny framework PBT.Tak więc, jeśli chcesz znaleźć dobry sposób na znalezienie bardziej skomplikowanych przypadków, o których możesz nie pomyśleć, jednoczesne korzystanie z umów i testów opartych na nieruchomościach bardzo pomoże. Oczywiście nie zastępuje to pisania testów jednostkowych, ale je rozszerza, co jest naprawdę najlepszym, co możemy zrobić jako inżynierowie.
źródło
/(today)|(yesterday)|([2-6] days ago)|...
), a następnie możesz uruchomić proces z losowo wybranymi danymi wejściowymi, aż znajdziesz takie, które nie znajduje się w zestawie oczekiwanych danych wyjściowych. Tego rodzaju podejście byłoby połów ten błąd i nie wymaga wiedząc, że błąd może istnieć wcześniej.Jest to przykład, w którym przydatne byłoby dodanie odrobiny modułowości. Jeśli segment kodu podatny na błędy jest używany wiele razy, dobrą praktyką jest zawinięcie go w funkcję, jeśli to możliwe.
źródło
TDD działa najlepiej jako technika, jeśli osoba pisząca testy jest przeciwna. Jest to trudne, jeśli nie programujesz w parach, więc innym sposobem myślenia na ten temat jest:
Jest to inna sztuka, która dotyczy pisania poprawnego kodu z TDD lub bez, i może być tak złożona (jeśli nie bardziej) niż pisanie kodu. Jest to coś, co musisz ćwiczyć, i jest to coś, na co nie ma jednej, łatwej, prostej odpowiedzi.
Podstawowa technika pisania solidnego oprogramowania, jest także podstawową techniką pozwalającą zrozumieć, jak pisać skuteczne testy:
Zrozumienie warunków wstępnych dla funkcji - poprawne stany (tj. Jakie założenia przyjmujesz na temat stanu klasy, dla której funkcja jest metodą) oraz prawidłowe zakresy parametrów wejściowych - każdy typ danych ma zakres możliwych wartości - których podzbiór będzie obsługiwane przez twoją funkcję.
Jeśli po prostu nie robisz nic poza jawnym testowaniem tych założeń przy wprowadzaniu funkcji i upewniając się, że naruszenie jest rejestrowane lub zgłaszane i / lub funkcja nie działa poprawnie, bez dalszej obsługi możesz szybko dowiedzieć się, czy Twoje oprogramowanie nie działa prawidłowo, spraw, aby było ono solidne i tolerancyjny na błędy, i rozwijaj swoje umiejętności testowania przeciwników.
NB. Istnieje cała literatura na temat warunków wstępnych i końcowych, niezmienników itp., A także bibliotek, które mogą je stosować za pomocą atrybutów. Osobiście nie przepadam za formalnym podejściem, ale warto się temu przyjrzeć.
źródło
Jest to jeden z najważniejszych faktów dotyczących tworzenia oprogramowania: napisanie kodu wolnego od błędów jest absolutnie absolutnie niemożliwe.
TDD nie uratuje cię przed wprowadzeniem błędów odpowiadających przypadkom testowym, o których nie pomyślałeś. Nie uchroni cię też przed napisaniem niepoprawnego testu, nie zdając sobie z tego sprawy, a następnie napisaniem niepoprawnego kodu, który zda pozytywny wynik testu błędnego. I każda inna technika tworzenia oprogramowania, jaką kiedykolwiek stworzono, ma podobne dziury. Jako programiści jesteśmy niedoskonałymi ludźmi. Na koniec dnia nie ma możliwości napisania kodu w 100% wolnego od błędów. Nigdy nie było i nigdy się nie wydarzy.
Nie oznacza to, że powinieneś porzucić nadzieję. Chociaż napisanie całkowicie idealnego kodu jest niemożliwe, bardzo możliwe jest napisanie kodu zawierającego tak mało błędów, które pojawiają się w tak rzadkich przypadkach, że oprogramowanie jest niezwykle praktyczne w użyciu. Oprogramowanie, które nie wykazuje błędów w praktyce, jest bardzo możliwe do napisania.
Ale pisanie tego wymaga od nas zaakceptowania faktu, że będziemy produkować błędne oprogramowanie. Prawie każda nowoczesna praktyka tworzenia oprogramowania opiera się na pewnym poziomie albo na zapobieganiu pojawianiu się błędów, albo na ochronie przed konsekwencjami błędów, które nieuchronnie produkujemy:
Ostatecznym rozwiązaniem zidentyfikowanego problemu nie jest walka z faktem, że nie możesz zagwarantować, że napiszesz kod wolny od błędów, ale raczej przyjęcie go. Zastosuj najlepsze praktyki branżowe we wszystkich obszarach procesu programowania, a będziesz konsekwentnie dostarczać użytkownikom kod, który, choć nie do końca idealny, jest wystarczająco solidny, aby sprostać zadaniu.
źródło
Po prostu wcześniej nie pomyślałeś o tym przypadku i dlatego nie miałeś na to przypadku testowego.
Zdarza się to cały czas i jest po prostu normalne. Zawsze jest to kompromis, ile wysiłku wkładasz w tworzenie wszystkich możliwych przypadków testowych. Możesz spędzić nieskończony czas na rozważenie wszystkich przypadków testowych.
W przypadku autopilota samolotowego spędziłbyś znacznie więcej czasu niż w przypadku prostego narzędzia.
Często pomaga pomyśleć o prawidłowych zakresach zmiennych wejściowych i przetestować te granice.
Ponadto, jeśli testerem jest inna osoba niż programista, często występują bardziej znaczące przypadki.
źródło
To kolejny logiczny błąd w kodzie, dla którego nie masz jeszcze testu jednostkowego :) - Twoja metoda zwróci nieprawidłowe wyniki dla użytkowników w strefach czasowych innych niż UTC. Przed obliczeniem musisz przekonwertować zarówno „teraz”, jak i datę wydarzenia na lokalną strefę czasową użytkownika.
Przykład: w Australii zdarzenie ma miejsce o 9 rano czasu lokalnego. O godzinie 11:00 zostanie wyświetlony jako „wczoraj”, ponieważ zmieniła się data UTC.
źródło
Niech ktoś napisze testy. W ten sposób ktoś nieznajomy twojej implementacji może sprawdzić rzadkie sytuacje, o których nawet nie pomyślałeś.
Jeśli to możliwe, wstrzyknij przypadki testowe jako kolekcje. To sprawia, że dodanie kolejnego testu jest tak proste, jak dodanie kolejnej linii
yield return new TestCase(...)
. Może to pójść w kierunku testów eksploracyjnych , automatyzując tworzenie przypadków testowych: „Zobaczmy, co kod zwraca za wszystkie sekundy sprzed tygodnia”.źródło
Wygląda na to, że jesteś w błędnym przekonaniu, że jeśli wszystkie twoje testy zakończą się pomyślnie, nie będziesz mieć żadnych błędów. W rzeczywistości, jeśli wszystkie testy przejdą pomyślnie, wszystkie znane zachowania są prawidłowe. Nadal nie wiesz, czy nieznane zachowanie jest prawidłowe, czy nie.
Mamy nadzieję, że używasz pokrycia kodu w TDD. Dodaj nowy test dla nieoczekiwanego zachowania. Następnie możesz uruchomić tylko test nieoczekiwanego zachowania, aby zobaczyć, jaką ścieżkę faktycznie przechodzi przez kod. Gdy poznasz bieżące zachowanie, możesz wprowadzić zmiany, aby je poprawić, a gdy wszystkie testy przejdą ponownie, będziesz wiedział, że zrobiłeś to poprawnie.
To wciąż nie oznacza, że twój kod jest wolny od błędów, tylko że jest lepszy niż wcześniej i po raz kolejny wszystkie znane zachowania są prawidłowe!
Prawidłowe używanie TDD nie oznacza, że napiszesz kod wolny od błędów, to znaczy, że napiszesz mniej błędów. Mówisz:
Czy to oznacza, że w wymaganiach określono zachowanie więcej niż jednego dnia, ale nie wczoraj? Jeśli przegapiłeś pisemne wymaganie, to twoja wina. Jeśli zdałeś sobie sprawę, że wymagania były niekompletne podczas kodowania, to dobrze dla Ciebie! Jeśli wszyscy, którzy pracowali nad wymaganiami, przeoczyli tę sprawę, nie jesteś gorszy od innych. Wszyscy popełniają błędy, a im są bardziej subtelni, tym łatwiej je przegapić. Wielką zaletą jest to, że TDD nie zapobiega wszystkim błędom!
źródło
Tak. Rozwój oparty na testach tego nie zmienia. Nadal możesz tworzyć błędy w rzeczywistym kodzie, a także w kodzie testowym.
Och, ale tak się stało! Przede wszystkim, kiedy zauważyłeś błąd, masz już pełną platformę testową i po prostu musiałeś naprawić błąd w teście (i rzeczywisty kod). Po drugie, nie wiesz, o ile więcej błędów miałbyś, gdybyś nie zrobił TDD na początku.
Nie możesz Nawet NASA nie znalazła sposobu na uniknięcie błędów; my, pomniejsi ludzie, na pewno też nie.
To jest błąd. Jedną z największych zalet TDD jest to, że możesz kodować mniej zastanawiając się, ponieważ wszystkie te testy przynajmniej dość dobrze rejestrują regresje. Ponadto, nawet, a zwłaszcza w przypadku TDD, nie oczekuje się dostarczania kodu wolnego od błędów w pierwszej kolejności (w przeciwnym razie prędkość rozwoju po prostu się zatrzyma).
Byłoby to wyraźnie sprzeczne z zasadą kodowania tylko tego, czego aktualnie potrzebujesz. Myślałeś, że potrzebujesz tych skrzynek i tak też było. To był niekrytyczny fragment kodu; jak powiedziałeś, nie było żadnych obrażeń, z wyjątkiem tego, że zastanawiałeś się przez 30 minut.
W przypadku kodu o znaczeniu krytycznym możesz zrobić to, co powiedziałeś, ale nie w przypadku standardowego kodu na co dzień.
Ty nie. Ufasz swoim testom, aby znaleźć większość regresji; trzymasz się cyklu refaktora czerwono-zielonego, pisząc testy przed / podczas faktycznego kodowania, i (ważne!) wdrażasz minimalną ilość niezbędną do przełączenia czerwono-zielonego (nie więcej, nie mniej). To zakończy się świetnym testem, przynajmniej pozytywnym.
Kiedy, a nie jeśli, znajdziesz błąd, piszesz test, aby go odtworzyć, i naprawić błąd przy minimalnym nakładzie pracy, aby wspomniany test zmienił kolor z czerwonego na zielony.
źródło
Właśnie odkryłeś, że bez względu na to, jak bardzo się starasz, nigdy nie będziesz w stanie wychwycić wszystkich możliwych błędów w kodzie.
Oznacza to, że nawet próba wyłapania wszystkich błędów jest ćwiczeniem daremnym, dlatego powinieneś używać tylko technik takich jak TDD jako sposobu pisania lepszego kodu, kodu zawierającego mniej błędów, a nie błędów 0.
To z kolei oznacza, że powinieneś poświęcić mniej czasu na użycie tych technik i zaoszczędzić czas pracując nad alternatywnymi sposobami znajdowania błędów, które prześlizgują się przez sieć rozwoju.
alternatywy, takie jak testowanie integracji lub zespół testowy, testowanie systemu oraz rejestrowanie i analizowanie tych dzienników.
Jeśli nie możesz złapać wszystkich błędów, musisz mieć strategię łagodzenia skutków błędów, które przejeżdżają obok ciebie. Jeśli i tak musisz to zrobić, to włożenie w to większego wysiłku ma większy sens niż próba (na próżno) zatrzymania ich w pierwszej kolejności.
W końcu bezcelowe wydawanie fortuny na pisanie testów i pierwszego dnia, w którym oddajesz swój produkt klientowi, przewraca się, szczególnie jeśli nie masz pojęcia, jak znaleźć i rozwiązać ten błąd. Usuwanie błędów pośmiertnych i po dostarczeniu jest tak ważne i wymaga więcej uwagi niż większość ludzi poświęca na pisanie testów jednostkowych. Zapisz testy jednostkowe dla skomplikowanych bitów i nie próbuj perfekcji z góry.
źródło
That in turn means you should spend less time using these techniques
- ale właśnie powiedziałeś, że to pomoże z mniejszą liczbą błędów ?!