Jak uniknąć logicznych błędów w kodzie, gdy TDD nie pomogło?

67

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ż deltajest 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?

Arseni Mourzenko
źródło
38
Mając na to przypadek testowy? Wygląda na to, jak później to odkryłeś i łączy się z TDD.
Οurous
63
Właśnie dowiedziałeś się, dlaczego nie jestem fanem testów opartych na testach - z mojego doświadczenia wynika, że ​​większość błędów złapanych podczas produkcji to scenariusze, o których nikt nie pomyślał. Rozwój oparty na testach i testy jednostkowe nic na to nie poradzą. (Testy jednostkowe mają jednak wartość w wykrywaniu błędów wprowadzanych w przyszłych edycjach.)
Loren Pechtel
102
Powtórz za mną: „Nie ma srebrnych kul, w tym TDD”. Nie ma żadnego procesu, żadnego zestawu reguł, żadnego algorytmu, którego można by robotycznie przestrzegać, aby stworzyć idealny kod. Gdyby tak było, moglibyśmy zautomatyzować cały proces i wykonać go.
jpmc26
43
Gratulacje, odkryłeś na nowo starą mądrość, że żadne testy nie mogą udowodnić braku błędów. Ale jeśli szukasz technik umożliwiających lepsze pokrycie możliwej domeny wejściowej, musisz przeprowadzić dokładną analizę domeny, przypadków brzegowych i klas równoważności tej domeny. Wszystkie stare, dobrze znane techniki, znane na długo przed wynalezieniem terminu TDD.
Doc Brown
80
Nie staram się być wredny, ale twoje pytanie wydaje się być sformułowane w następujący sposób: „jak myślę o rzeczach, o których nie myślałem?”. Nie jestem pewien, co to ma wspólnego z TDD.
Jared Smith

Odpowiedzi:

57

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):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

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_unitzrobić, jeśli deltajest negatywne?

Innymi słowy, refaktoryzacja nie służy tylko upiększaniu. Ułatwia ludziom wykrycie błędów, których nie potrafi kompilator.

Karl Bielefeldt
źródło
12
Następnym krokiem jest internacjonalizacja, a pluralizepraca tylko dla podzbioru angielskich słów będzie odpowiedzialnością.
Deduplicator
@Deduplicator na pewno, ale potem, w zależności od tego, na które języki / kultury celujesz, możesz uniknąć modyfikacji i pluralizeużycia numi unitzbudowania jakiegoś klucza, aby pobrać ciąg formatu z pliku tabeli / zasobu. LUB możesz potrzebować pełnego przepisania logiki, ponieważ potrzebujesz różnych jednostek ;-)
Hulk
4
Problemem pozostaje nawet ta refaktoryzacja, która polega na tym, że „wczoraj” nie ma większego sensu w bardzo wczesnych godzinach porannych (krótko po 12:01). Z przyjaznego punktu widzenia człowieka coś, co wydarzyło się o 23:59, nie zmienia się nagle z „dzisiaj” na „wczoraj”, kiedy zegar mija po północy. Zamiast tego zmienia się z „1 minutę temu” na „2 minuty temu”. „Dzisiaj” jest zbyt grubiańskie, jeśli chodzi o coś, co wydarzyło się kilka minut temu, a „wczoraj” jest pełne problemów nocnych sów.
David Hammen
@DavidHammen Jest to problem z użytecznością i zależy od tego, jak precyzyjnie musisz być. Gdy chcesz wiedzieć co najmniej do godziny, nie sądzę, że „wczoraj” jest dobre. „24 godziny temu” jest znacznie wyraźniejsze i jest powszechnie używanym ludzkim wyrażeniem w celu podkreślenia liczby godzin. Komputery, które starają się być „przyjazne dla człowieka”, prawie zawsze mylą się i nadmiernie uogólniają na „wczoraj”, co jest zbyt niejasne. Ale aby to wiedzieć, musisz przeprowadzić wywiad z użytkownikami, aby zobaczyć, co myślą. W przypadku niektórych rzeczy naprawdę potrzebujesz dokładnej daty i godziny, więc „wczoraj” jest zawsze błędne.
Brandin
149

Rozwój oparty na testach nie pomógł.

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.

ezoterik
źródło
2
Można by powiedzieć, że gdyby deweloper nie użył TDD, znacznie bardziej prawdopodobne byłoby pominięcie innych przypadków.
Caleb
75
A poza tym pomyśl o tym, ile czasu zaoszczędzono, kiedy naprawiliśmy błąd? Dzięki wdrożeniu istniejących testów od razu wiedzieli, że ich zmiana nie złamała istniejącego zachowania. I mogli swobodnie dodawać nowe przypadki testowe i refaktoryzować bez konieczności późniejszego przeprowadzania obszernych testów ręcznych.
Caleb
15
TDD jest tak dobre, jak napisane testy.
Mindwin
Kolejna obserwacja: dodanie testu dla tego przypadku poprawi projekt, zmuszając nas do datetime.utcnow()usunięcia go z funkcji, a zamiast tego przekazania nowjako (odtwarzalnego) argumentu.
Toby Speight
114

wydarzenie, które miało miejsce dwadzieścia sześć godzin temu, miałoby miejsce jeden dzień temu

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.

Kevin Krumwiede
źródło
45
To świetny punkt do zrobienia. Brak wymagania nie oznacza koniecznie, że proces wdrożenia nie powiódł się. Oznacza to po prostu, że wymaganie nie zostało dobrze zdefiniowane. (Albo po prostu popełniłeś błąd ludzki, co zdarza się od czasu do czasu)
Caleb
Oto odpowiedź, którą chciałem udzielić. Zdefiniowałbym specyfikację jako „gdyby wydarzenie było w tym dniu kalendarzowym, obecna delta w godzinach. W innym przypadku użyj dat tylko do ustalenia delty” Testowanie godzin jest przydatne tylko w ciągu jednego dnia, jeśli poza tym twoja rozdzielczość ma być dniami.
Baldrickk
1
Podoba mi się ta odpowiedź, ponieważ wskazuje na prawdziwy problem: punkty w czasie i dacie są dwiema różnymi wielkościami. Są ze sobą powiązane, ale kiedy zaczniesz je porównywać, rzeczy idą bardzo szybko na południe. W programowaniu logika daty i godziny jest jedną z najtrudniejszych rzeczy do zrobienia. Naprawdę nie podoba mi się, że wiele wdrożeń daty zasadniczo zapisuje datę jako 0:00 punktu w czasie. Powoduje to wiele zamieszania.
Pieter B
38

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ę?

Ian Jacobs
źródło
2
Posiadanie testów napisanych przez kogoś innego niż programista jest zawsze dobrym pomysłem, oznacza to, że obie strony muszą przeoczyć ten sam warunek wejściowy, aby błąd mógł zostać wprowadzony do produkcji.
Michael Kay,
35

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ę.

Cbojar
źródło
9
Jest to bardzo ważna część TDD, która jest często pomijana i rzadko widywałem o niej w artykułach i przewodnikach - bardzo ważne jest testowanie przypadków skrajnych i warunków brzegowych, ponieważ uważam, że jest to źródło 90% błędów - od-by -on błędy, ponad i cieków, ostatni dzień miesiąca, w ostatnim miesiącu roku, LEAP roku etc etc
GoatInTheMachine
2
@GoatInTheMachine - i 90% z tych 90% błędów dotyczy zmian czasu w ciągu dnia ..... Hahaha
Caleb
1
Najpierw możesz podzielić możliwe dane wejściowe na klasy równoważności, a następnie określić przypadki brzegowe na granicach klas. Nasz wysiłek jest większy niż wysiłek rozwojowy; to, czy warto, zależy od tego, jak ważne jest dostarczanie oprogramowania możliwie bezbłędnie, jaki jest termin i ile masz pieniędzy i cierpliwości.
Peter A. Schneider,
2
To jest poprawna odpowiedź. Wiele reguł biznesowych wymaga podzielenia zakresu wartości na przedziały, w których są to przypadki, które należy traktować na różne sposoby.
abuzittin gillifirca
14

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ć).

Polygnome
źródło
12

Jedynym sposobem, jaki mogę wymyślić, jest dodanie wielu stwierdzeń dotyczących 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 powtarzanie co sekundę przez ostatnie dziesięć lat, sprawdzanie każde naruszenie stwierdzeń, które wydaje się zbyt skomplikowane.

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 listy l, 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.

Hovercouch
źródło
2
To jest dokładnie właściwe rozwiązanie tego rodzaju problemu. Zestaw prawidłowych danych wyjściowych jest łatwy do zdefiniowania (możesz podać wyrażenie regularne w bardzo prosty sposób, coś podobnego /(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.
Jules
@Jules Zobacz także sprawdzanie / testowanie właściwości . Zwykle piszę testy własności podczas programowania, aby uwzględnić jak najwięcej nieprzewidzianych przypadków i zmusić mnie do myślenia o ogólnych właściwościach / niezmiennikach.
Zapisuję
1
Jeśli wykonujesz tyle zapętleń w testach, jeśli zajmie to bardzo dużo czasu, co pokonuje jeden z głównych celów testów jednostkowych: szybko uruchom testy !
CJ Dennis
5

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.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

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 time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
Antonio Perez
źródło
5

Rozwój oparty na testach nie pomógł.

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:

  • Nie pisz testów, aby potwierdzić, że testowana funkcja działa tak, jak ją wykonałeś. Napisz testy, które celowo je zepsują.

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ć.

Chris Becke
źródło
1

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:

  • Zbieranie dokładnych wymagań pozwala nam wiedzieć, jak wygląda nieprawidłowe zachowanie w naszym kodzie.
  • Pisanie czystego, starannie zaprojektowanego kodu ułatwia uniknięcie wprowadzania błędów i łatwiej je naprawić, gdy je zidentyfikujemy.
  • Pisanie testów pozwala nam stworzyć zapis tego, co naszym zdaniem byłoby najgorszymi możliwymi błędami w naszym oprogramowaniu i udowodnić, że unikamy przynajmniej tych błędów. TDD produkuje te testy przed kodem, BDD wywodzi je z wymagań, a staromodne testy jednostkowe produkują testy po napisaniu kodu, ale wszystkie zapobiegają najgorszym regresom w przyszłości.
  • Oceny wzajemne oznaczają, że za każdym razem, gdy kod jest zmieniany, co najmniej dwie pary oczu go widzą, zmniejszając tym samym, jak często błędy wpadają do mastera.
  • Używanie narzędzia do śledzenia błędów lub śledzenia historii użytkownika, które traktuje błędy jako historie użytkownika, oznacza, że ​​gdy pojawiają się błędy, są one śledzone i ostatecznie usuwane, nie zapominając o nich i pozostawiając je konsekwentnie na drodze użytkowników.
  • Korzystanie z serwera pomostowego oznacza, że ​​przed wydaniem głównym wszelkie błędy show-stopper mają szansę się pojawić i zostać usunięte.
  • Korzystanie z kontroli wersji oznacza, że ​​w najgorszym scenariuszu, w którym kod z poważnymi błędami jest wysyłany do klientów, możesz wykonać awaryjne wycofanie i uzyskać niezawodny produkt z powrotem w ręce swoich klientów, podczas gdy Ty coś załatwisz.

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.

Kevin
źródło
1

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.

Szymon
źródło
1

(i przekonanie, że ma to związek ze strefami czasowymi, pomimo jednolitego użycia UTC w kodzie)

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.

Siergiej
źródło
0
  • 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”.

zero
źródło
0

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:

Wymagania były stosunkowo jasne

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!

CJ Dennis
źródło
0

Bardzo łatwo jest popełnić błąd logiczny nawet w tak prostym kodzie źródłowym.

Tak. Rozwój oparty na testach tego nie zmienia. Nadal możesz tworzyć błędy w rzeczywistym kodzie, a także w kodzie testowym.

Rozwój oparty na testach nie pomógł.

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.

Niepokojące jest również to, że nie widzę, jak można uniknąć takich błędów.

Nie możesz Nawet NASA nie znalazła sposobu na uniknięcie błędów; my, pomniejsi ludzie, na pewno też nie.

Pomijając więcej myślenia przed napisaniem kodu,

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).

jedynym sposobem, jaki mogę wymyślić, jest dodanie wielu stwierdzeń dotyczących 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ądanie co sekundę przez ostatnie dziesięć lat, sprawdzanie każde naruszenie stwierdzeń, które wydaje się zbyt skomplikowane.

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ń.

Jak mogę uniknąć tworzenia tego błędu?

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.

AnoE
źródło
-2

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.

gbjbaanb
źródło
To jest wyjątkowo pokonane. 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 ?!
JᴀʏMᴇᴇ
@ JᴀʏMᴇᴇ bardziej pragmatyczne podejście, która technika daje ci największe zyski. Znam ludzi, którzy są dumni, że spędzają 10 razy na pisaniu testów, niż na kodzie, i nadal mają błędy. Więc będąc rozsądnym, a nie dogmatycznym, na temat technik testowania jest niezbędna. I tak należy zastosować testy integracyjne, więc włóż w nie więcej wysiłku niż w testowanie jednostkowe.
gbjbaanb