Jak wykonać programowanie oparte na testach

15

Mam ponad 2-letnie doświadczenie w tworzeniu aplikacji. W ciągu tych dwóch lat moje podejście do rozwoju było następujące

  1. Analizuj wymagania
  2. Tożsamość Główny komponent / Obiekty, Wymagane funkcje, Zachowanie, Proces i ich ograniczenia
  3. Twórz klasy, relacje między nimi, ograniczenia dotyczące zachowania i stanów obiektów
  4. Twórz funkcje, przetwarzaj z ograniczeniami behawioralnymi zgodnie z wymaganiami
  5. Ręcznie przetestuj aplikację
  6. Jeśli zmiany wymagań zmodyfikują komponent / funkcje, należy ręcznie przetestować aplikację

Niedawno zapoznałem się z TDD i uważam, że jest to bardzo dobry sposób na programowanie, ponieważ opracowany kod ma poważne powody, aby istnieć, a wiele problemów po wdrożeniu zostało zmniejszonych.

Ale mój problem polega na tym, że nie jestem w stanie najpierw utworzyć testów, raczej identyfikuję komponenty i po prostu piszę dla nich test, zanim zacznę pisać komponenty. moje pytanie brzmi

  1. Czy robię to, prawda? Jeśli nie to co dokładnie muszę zmienić
  2. Czy jest jakiś sposób na stwierdzenie, czy napisany test jest wystarczający?
  3. Czy dobrą praktyką jest pisanie testu pod kątem bardzo prostej funkcjonalności, która może być równoważna 1 + 1 = 2, czy może to tylko overplay?
  4. Czy dobrze jest zmienić funkcjonalność i odpowiednio sprawdzić, czy zmieniają się wymagania?
Jogesh
źródło
2
„Identyfikuję komponenty i piszę dla nich test, zanim zacznę pisać komponenty.”: Uważam to za poprawne: najpierw identyfikujesz zgrubną architekturę twojego systemu, a następnie zaczynasz kodować. Podczas kodowania (TDD) opracowujesz szczegóły poszczególnych komponentów i ewentualnie odkrywasz problemy z architekturą, które możesz rozwiązać po drodze. Uważam jednak, że nie zaczynasz kodować bez uprzedniej analizy.
Giorgio
Możesz także przeprowadzić automatyczne testy jednostkowe / integracyjne bez TDD. Obaj są często zdezorientowani, ale to nie to samo.
Andres F.,

Odpowiedzi:

19

Czy robię to, prawda? Jeśli nie to co dokładnie muszę zmienić

Trudno powiedzieć na podstawie tego krótkiego opisu, ale podejrzewam, że nie robisz tego dobrze. Uwaga: nie mówię, że to, co robisz, nie działa lub jest w jakiś sposób złe, ale nie robisz TDD. Środkowe „D” oznacza „Driven”, testy napędzają wszystko, proces rozwoju, kod, projekt, architekturę, wszystko .

Testy mówią ci, co napisać, kiedy to napisać, co napisać dalej, kiedy przestać pisać. Mówią o projekcie i architekturze. (Projekt i architektura wyłaniają się z kodu poprzez refaktoryzację.) TDD nie polega na testowaniu. Tu nawet nie chodzi o pisanie testów: TDD polega na tym, aby testy cię poprowadziły, napisanie ich jako pierwszego jest po prostu niezbędnym warunkiem do tego.

Nie ma znaczenia, czy faktycznie zapisujesz kod, czy masz go w pełni rozwinięty: piszesz (szkielety) kodu w głowie, a następnie piszesz testy dla tego kodu. To nie TDD.

Porzucenie tego nawyku jest trudne . Naprawdę bardzo ciężko. Wydaje się to szczególnie trudne dla doświadczonych programistów.

Keith Braithwaite stworzył ćwiczenie, które nazywa TDD tak, jakbyś to rozumiał . Składa się z zestawu zasad (opartych na trzech regułach TDD wuja Boba Martina , ale o wiele surowszych), których należy ściśle przestrzegać i które mają na celu bardziej rygorystyczne stosowanie TDD. Działa najlepiej z programowaniem par (aby twoja para mogła upewnić się, że nie łamiesz zasad) i instruktorem.

Reguły są następujące:

  1. Napisz dokładnie jeden nowy test, najmniejszy test, jaki możesz wskazać w kierunku rozwiązania
  2. Zobacz, jak zawodzi; niepowodzenia kompilacji liczą się jako awarie
  3. Wykonaj test z (1), pisząc najmniejszy możliwy kod implementacyjny w metodzie testowej .
  4. Przeprowadź refaktoryzację, aby usunąć duplikację, i w inny sposób, aby poprawić projekt. Bądź ostrożny przy użyciu tych ruchów:
    1. chcesz nowej metody - poczekaj do czasu refaktoryzacji, a następnie… utwórz nowe (nietestowe) metody, wykonując jedną z tych czynności i w żaden inny sposób:
      • preferowane: wykonaj Wyodrębnij metodę z kodu implementacji utworzonego zgodnie z (3), aby utworzyć nową metodę w klasie testowej, lub
      • jeśli musisz: przenieść kod implementacji zgodnie z (3) do istniejącej metody implementacji
    2. chcesz nowej klasy - poczekaj do czasu refaktoryzacji, a następnie… utwórz klasy nietestowe, aby zapewnić miejsce docelowe dla metody przenoszenia bez żadnego innego powodu
    3. zapełnij klasy implementacji metodami, wykonując metodę Move, i żadną inną metodą

Zazwyczaj doprowadzi to do bardzo różnych projektów niż często praktykowana „pseudo-TDD” polegająca na wyobrażaniu sobie w głowie, jaki powinien być projekt, a następnie pisanie testów w celu wymuszenia tego projektu, wdrożenie projektu, który już przewidziałeś przed napisaniem testy ”.

Kiedy grupa ludzi wdraża coś w rodzaju gry w kółko i krzyżyk za pomocą pseudo-TDD, zwykle kończy się to bardzo podobnymi projektami, obejmującymi jakąś Boardklasę z tablicą 3 × 3 Integers. I przynajmniej część programistów faktycznie napisała tę klasę bez testów, ponieważ „wiedzą, że będą jej potrzebować” lub „będą potrzebować czegoś, na co mogliby napisać swoje testy”. Jednak, gdy zmusisz tę samą grupę do zastosowania TDD tak, jak chcesz, często kończą się szeroką różnorodnością bardzo różnych projektów, często nie wykorzystując niczego nawet zdalnie podobnego do Board.

Czy jest jakiś sposób na stwierdzenie, czy napisany test jest wystarczający?

Gdy pokrywają wszystkie wymagania biznesowe. Testy są kodowaniem wymagań systemowych.

Czy dobrą praktyką jest pisanie testu pod kątem bardzo prostej funkcjonalności, która może być równoważna 1 + 1 = 2, czy może to tylko overplay?

Znowu masz to odwrotnie: nie piszesz testów funkcjonalności. Piszesz funkcjonalność do testów. Jeśli funkcjonalność umożliwiająca zdanie testu jest trywialna, to świetnie! Właśnie spełniłeś wymagania systemowe i nie musiałeś nawet ciężko nad tym pracować!

Czy dobrze jest zmienić funkcjonalność i odpowiednio sprawdzić, czy zmieniają się wymagania?

Nie. Odwrotnie. Jeśli wymaganie się zmienia, zmieniasz test, który odpowiada temu wymaganiu, obserwujesz, jak się nie udaje, a następnie zmieniasz kod, aby przejść pomyślnie. Testy są zawsze najważniejsze.

Trudno to zrobić. Potrzebujesz dziesiątek, może setek godzin celowej praktyki , aby zbudować coś w rodzaju „pamięci mięśniowej”, aby dojść do punktu, w którym, gdy zbliża się termin i jesteś pod presją, nawet nie musisz o tym myśleć , a robienie tego staje się najszybszym i najbardziej naturalnym sposobem pracy.

Jörg W Mittag
źródło
1
Rzeczywiście bardzo jasna odpowiedź! Z praktycznego punktu widzenia elastyczna i wydajna platforma testowa jest bardzo przyjemna podczas ćwiczenia TDD. Niezależnie od TDD możliwość automatycznego uruchamiania testów jest nieoceniona przy debugowaniu aplikacji. Aby rozpocząć korzystanie z TDD, programy nieinterakcyjne (w stylu UNIX) są prawdopodobnie najłatwiejsze, ponieważ przypadek użycia można przetestować, porównując status wyjścia i wyjście programu z oczekiwanymi. Konkretny przykład tego podejścia można znaleźć w mojej bibliotece Gasoline dla OCaml.
Michael Le Barbier Grünewald,
4
Mówisz „kiedy zmusisz tę samą grupę do zastosowania TDD tak, jak tego chcesz, często kończą się szeroką różnorodnością bardzo różnych wzorów, często nie wykorzystując niczego nawet zdalnie podobnego do tablicy”, jakby to była dobra rzecz . Nie jest dla mnie wcale jasne, że jest to dobra rzecz, a może nawet być złe z punktu widzenia konserwacji, ponieważ brzmi, jakby implementacja byłaby bardzo sprzeczna z intuicją dla kogoś nowego. Czy możesz wyjaśnić, dlaczego ta różnorodność implementacji jest dobra, a przynajmniej niezła?
Jim Clay,
3
+1 Odpowiedź jest dobra, ponieważ poprawnie opisuje TDD. Jednak pokazuje również, dlaczego TDD jest wadliwą metodologią: konieczne jest staranne przemyślenie i wyraźne zaprojektowanie, szczególnie w obliczu problemów algorytmicznych. Robienie TDD „na ślepo” (jak nakazuje TDD) przez udawanie, że nie ma wiedzy w dziedzinie, prowadzi do niepotrzebnych trudności i ślepych zaułków. Zobacz niesławną porażkę solvera Sudoku (krótka wersja: TDD nie może pobić wiedzy o domenie).
Andres F.,
1
@AndresF .: W rzeczywistości post na blogu, do którego prowadzisz link, wydaje się odzwierciedlać doświadczenia Keitha podczas korzystania z TDD, tak jakbyś to chciał: po zrobieniu „pseudo-TDD” dla kółko i krzyżyk, zaczynają od utworzenia Boardklasy z Tablica 3x3 ints (lub coś takiego). Natomiast jeśli zmusisz ich do wykonania TDDAIYMI, często kończą się tworzeniem mini-DSL do przechwytywania wiedzy o domenie. To oczywiście tylko anegdota. Przydatne byłyby statystycznie i naukowo uzasadnione badanie, ale jak to często bywa w przypadku takich badań, są one albo o wiele za małe, albo o wiele za drogie.
Jörg W Mittag
@ JörgWMittag Popraw mnie, jeśli cię źle zrozumiałem, ale czy mówisz, że Ron Jeffries robił „pseudo-TDD”? Czy to nie jest forma błędu „brak prawdziwego Szkota”? (Zgadzam się z tobą w sprawie potrzeby dalszych badań naukowych; blog, do którego prowadzę link, jest tylko kolorową anegdotą o spektakularnym niepowodzeniu konkretnego przypadku użycia TDD. Niestety wydaje się, że ewangeliści TDD są zbyt głośni, by reszta z nas, aby mieć prawdziwą analizę tej metholody i jej rzekomych korzyści).
Andres F.,
5

Opisujesz swoje podejście do programowania jako proces „z góry na dół” - zaczynasz od wyższego poziomu abstrakcji i coraz bardziej zagłębiasz się w szczegóły. TDD, przynajmniej w formie popularnej, jest techniką „oddolną”. A dla kogoś, kto pracuje głównie „z góry na dół”, może być bardzo nietypowe, aby pracować „z dołu do góry”.

Jak więc wnieść więcej „TDD” do procesu rozwoju? Po pierwsze, zakładam, że twój rzeczywisty proces rozwoju nie zawsze jest tak „odgórny”, jak to opisano powyżej. Po kroku 2 prawdopodobnie zidentyfikujesz niektóre komponenty, które są niezależne od innych komponentów. Czasami decydujesz się najpierw na wdrożenie tych komponentów. Szczegóły publicznego interfejsu API tych komponentów prawdopodobnie nie odpowiadają Twoim wymaganiom, szczegóły również podążają za decyzjami projektowymi. W tym miejscu możesz zacząć od TDD: wyobraź sobie, jak zamierzasz korzystać z komponentu i jak faktycznie będziesz korzystać z API. A kiedy zaczynasz kodować takie użycie interfejsu API w formie testu, właśnie zacząłeś od TDD.

Po drugie, możesz wykonywać TDD nawet wtedy, gdy zamierzasz kodować więcej „z góry na dół”, zaczynając od komponentów, które najpierw zależą od innych nieistniejących komponentów. Musisz nauczyć się, jak najpierw „wyśmiewać” te inne zależności. Umożliwi to tworzenie i testowanie komponentów wysokiego poziomu przed przejściem do komponentów niższego poziomu. Bardzo szczegółowy przykład robienia TDD w sposób odgórny można znaleźć w tym blogu Ralfa Westphala .

Doktor Brown
źródło
3

Czy robię to, prawda? Jeśli nie to co dokładnie muszę zmienić

Radzisz sobie dobrze.

Czy jest jakiś sposób na stwierdzenie, czy napisany test jest wystarczający?

Tak, użyj narzędzia pokrycia testowego / kodu . Martin Fowler oferuje kilka dobrych porad na temat pokrycia testowego.

Czy dobrą praktyką jest pisanie testu pod kątem bardzo prostej funkcjonalności, która może być równoważna 1 + 1 = 2, czy może to tylko overplay?

Ogólnie rzecz biorąc, każda funkcja, metoda, komponent itp., Które mogą przynieść pewien wynik, biorąc pod uwagę niektóre dane wejściowe, jest dobrym kandydatem do testu jednostkowego. Jednak, podobnie jak w przypadku większości rzeczy w życiu (inżynieryjnym), należy wziąć pod uwagę swoje kompromisy: czy wysiłek jest zrównoważony przez napisanie testu jednostkowego, co skutkuje bardziej stabilną bazą kodu na dłuższą metę? Ogólnie rzecz biorąc, najpierw wybierz kod testowy dla kluczowych / krytycznych funkcji. Później, jeśli okaże się, że istnieją błędy związane z pewną niesprawdzoną częścią kodu, dodaj więcej testów.

Czy dobrze jest zmienić funkcjonalność i odpowiednio sprawdzić, czy zmieniają się wymagania?

Zaletą posiadania automatycznych testów jest to, że natychmiast zobaczysz, czy zmiana złamie poprzednie twierdzenia. Jeśli oczekujesz tego ze względu na zmienione wymagania, tak, możesz zmienić kod testowy (w rzeczywistości w czystym TDD najpierw zmieniłbyś testy zgodnie z wymaganiami, a następnie dostosowałeś kod, aż spełni nowe wymagania).

miraculixx
źródło
Pokrycie kodu może nie być bardzo wiarygodną miarą. Egzekwowanie% zasięgu zwykle skutkuje wieloma niepotrzebnymi testami (takimi jak testy dla wszystkich parametrów kontroli zerowej itp. - które są testami ze względu na testy, które nie dodają prawie żadnej wartości) i zmarnowanym czasem programowania, podczas gdy trudny do przetestowania kod ścieżki mogą w ogóle nie być testowane.
Paul
3

Najpierw pisanie testów to zupełnie inne podejście do pisania oprogramowania. Testy są nie tylko narzędziem do prawidłowej weryfikacji funkcjonalności kodu (wszystkie przechodzą), ale siłą, która określa projekt. Chociaż zasięg testowy może być użyteczną miarą, nie może być celem samym w sobie - celem TDD nie jest uzyskanie dobrego% pokrycia kodu, ale pomyślenie o testowalności kodu przed jego napisaniem.

Jeśli najpierw masz problemy z pisaniem testów, zdecydowanie polecam wykonanie sesji programowania w parze z kimś, kto ma doświadczenie w TDD, abyś miał doświadczenie w „sposobie myślenia” o całym podejściu.

Inną dobrą rzeczą jest obejrzenie wideo online, w którym oprogramowanie jest rozwijane przy użyciu TDD od pierwszej linii. Dobry, który kiedyś przedstawiałem TDD, to Let's Play TDD Jamesa Shore'a. Spójrz, zilustruje to, jak działa powstające projektowanie, jakie pytania powinieneś sobie zadać podczas pisania testów oraz w jaki sposób nowe klasy i metody są tworzone, refaktoryzowane i powtarzane.

Czy jest jakiś sposób na stwierdzenie, czy napisany test jest wystarczający?

Uważam, że to niewłaściwe pytanie. Kiedy robisz TDD, wybrałeś TDD i nowy projekt jako sposób pisania oprogramowania. Jeśli jakakolwiek nowa funkcja, którą musisz dodać, zawsze zaczyna się od testu, zawsze będzie dostępna.

Czy dobrą praktyką jest pisanie testu pod kątem bardzo prostej funkcjonalności, która może być równoważna 1 + 1 = 2, czy może to tylko overplay?

Oczywiście to zależy, użyj własnego osądu. Wolę nie pisać testów na parametrach kontroli zerowej, jeśli metoda nie jest częścią publicznego API, ale inaczej, dlaczego nie potwierdziłbyś, że metoda Add (a, b) rzeczywiście zwraca a + b?

Czy dobrze jest zmienić funkcjonalność i odpowiednio sprawdzić, czy zmieniają się wymagania?

Ponownie, gdy zmieniasz lub dodajesz nową funkcjonalność do swojego kodu, zaczynasz od testu, niezależnie od tego, czy dodajesz nowy test, czy zmieniasz istniejący, gdy zmieniają się wymagania.

Paweł
źródło