Czy ma sens pisanie testów starszego kodu, gdy nie ma czasu na pełne refaktoryzowanie?

72

Zwykle staram się postępować zgodnie z zaleceniami zawartymi w książce Skutecznie współpracując z Legacy Cod e . Przełamuję zależności, przenoszę części kodu do @VisibleForTesting public staticmetod i nowych klas, aby kod (lub przynajmniej jego część) był testowalny. Piszę testy, aby upewnić się, że niczego nie popsuję podczas modyfikowania lub dodawania nowych funkcji.

Kolega mówi, że nie powinienem tego robić. Jego rozumowanie:

  • Pierwotny kod może nie działać poprawnie. A pisanie testów dla niego utrudnia przyszłe poprawki i modyfikacje, ponieważ deweloperzy również muszą rozumieć i modyfikować testy.
  • Jeśli jest to kod GUI z pewną logiką (na przykład ~ 12 wierszy, na przykład 2-3 blok if / else), test nie jest wart problemów, ponieważ kod jest zbyt trywialny na początek.
  • Podobne złe wzorce mogą również występować w innych częściach bazy kodu (których jeszcze nie widziałem, jestem raczej nowy); łatwiej będzie wyczyścić je wszystkie w jednym dużym refaktoryzacji. Wyodrębnienie logiki może podważyć tę przyszłą możliwość.

Czy powinienem unikać wydobywania części do testowania i pisania testów, jeśli nie mamy czasu na pełne refaktoryzowanie? Czy powinienem wziąć pod uwagę jakąś wadę?

is4
źródło
29
Wygląda na to, że twój kolega po prostu przedstawia wymówki, ponieważ nie działa w ten sposób. Ludzie czasami zachowują się w ten sposób, ponieważ są zbyt wytrwali, aby zmienić przyjęty sposób robienia rzeczy.
Doc Brown
3
na to, co powinno zostać uznane za błąd, mogą polegać inne części kodu, zmieniając go w funkcję
maniak zapadkowy
1
Jedynym dobrym argumentem przeciwko temu, o czym mogę myśleć, jest to, że samo refaktoryzowanie może wprowadzić nowe błędy, jeśli coś źle odczytałeś / pomyliłeś. Z tego powodu mogę dowolnie modyfikować i poprawiać treść mojego serca w aktualnie opracowywanej wersji - ale wszelkie poprawki w poprzednich wersjach napotykają znacznie większą przeszkodę i mogą nie zostać zatwierdzone, jeśli są „tylko” kosmetycznym / strukturalnym porządkiem od czasu ryzyko uznaje się za przekraczające potencjalny zysk. Poznaj swoją lokalną kulturę - nie tylko pomysł jednego krowa-orkera - i przygotuj WYJĄTKOWO silne powody, zanim zrobisz cokolwiek innego.
keshlam
6
Pierwsza kwestia jest w pewnym sensie przezabawna - „Nie testuj, może być wadliwa”. No tak? To dobrze wiedzieć - albo chcemy to naprawić, albo nie chcemy, aby ktokolwiek zmienił rzeczywiste zachowanie na to, co mówi niektóre specyfikacje projektu. Tak czy inaczej, testowanie (i uruchamianie testów w zautomatyzowanym systemie) jest korzystne.
Christopher Creutzig
3
Zbyt często „jeden wielki refaktoryzacja”, który ma się wkrótce wydarzyć i który wyleczy wszystkie choroby, jest mitem wymyślonym przez tych, którzy po prostu chcą pchać rzeczy, które uważają za nudne (pisanie testów) w daleką przyszłość. A jeśli kiedykolwiek stanie się to rzeczywistością, poważnie będą żałować, że stało się tak duże!
Julia Hayward,

Odpowiedzi:

100

Oto moje osobiste nienaukowe wrażenie: wszystkie trzy powody brzmią jak rozpowszechnione, ale fałszywe złudzenia poznawcze.

  1. Jasne, istniejący kod może być nieprawidłowy. To może również mieć rację. Ponieważ aplikacja jako całość wydaje się mieć dla ciebie wartość (w przeciwnym razie po prostu ją odrzucisz), w przypadku braku bardziej szczegółowych informacji należy założyć, że jest ona w przeważającej mierze słuszna. „Pisanie testów sprawia, że ​​wszystko staje się trudniejsze, ponieważ w grę wchodzi więcej kodu” to proste i bardzo błędne podejście.
  2. Z pewnością należy poświęcić wysiłki związane z refaktoryzacją, testowaniem i ulepszaniem w miejscach, w których przynoszą największą wartość przy najmniejszym wysiłku. Podprogramy GUI formatujące wartości często nie są priorytetem. Ale nie testowanie czegoś, ponieważ „to proste” jest również bardzo złym podejściem. Popełniane są praktycznie wszystkie poważne błędy, ponieważ ludzie sądzą, że rozumieją coś lepszego niż w rzeczywistości.
  3. „Zrobimy to wszystko za jednym zamachem w przyszłości” - to miła myśl. Zwykle duże uderzenie pozostaje mocno w przyszłości, podczas gdy w chwili obecnej nic się nie dzieje. Ja jestem zdecydowanie przekonany o „powolnym i stałym wygrywaniu wyścigu”.
Kilian Foth
źródło
23
+1 za „Praktycznie wszystkie poważne błędy są popełniane, ponieważ ludzie myśleli, że rozumieją coś lepszego niż w rzeczywistości”.
rem
Do punktu 1 - w przypadku BDD testy są samok dokumentujące ...
Robbie Dee
2
Jak wskazuje @ guillaume31, częścią wartości pisania testów jest demonstrowanie, jak kod faktycznie działa - co może, ale nie musi być zgodne ze specyfikacją. Ale może to być „niewłaściwa” specyfikacja: potrzeby biznesowe mogły ulec zmianie, a kod odzwierciedla nowe wymagania, ale specyfikacja nie. Samo założenie, że kod jest „zły”, jest zbyt uproszczone (patrz punkt 1). I znowu testy pokażą ci, co faktycznie robi kod, a nie to, co ktoś myśli / mówi, że to robi (patrz punkt 2).
David
nawet jeśli wykonasz jedno uderzenie, musisz zrozumieć kod. Testy pomogą ci wychwycić nieoczekiwane zachowanie, nawet jeśli nie zmienisz faktury, ale przepisujesz (a jeśli zmienisz fakturę, pomagają upewnić się, że refaktoryzacja nie łamie starszego zachowania - lub tylko tam, gdzie chcesz, aby się zepsuła). Zapraszam do włączenia lub nie - jak chcesz.
Frank Hopkins
50

Kilka myśli:

Podczas refaktoryzacji starszego kodu nie ma znaczenia, czy niektóre pisane testy są sprzeczne z idealnymi specyfikacjami. Liczy się to, że testują bieżące zachowanie programu . Refaktoryzacja polega na podejmowaniu drobnych kroków izo-funkcjonalnych, aby kod był czystszy; nie chcesz angażować się w naprawianie błędów podczas refaktoryzacji. Poza tym, jeśli zauważysz rażący błąd, nie zostanie utracony. Zawsze możesz napisać dla niego test regresji i tymczasowo go wyłączyć lub wstawić zadanie naprawy błędów do swojego rejestru na później. Jedna rzecz na raz.

Zgadzam się, że czysty kod GUI jest trudny do przetestowania i być może nie nadaje się do refaktoryzacji typu „ Efektywnie działające ... ”. Nie oznacza to jednak, że nie należy wyodrębniać zachowania, które nie ma nic wspólnego z warstwą GUI, ani testować wyodrębnionego kodu. A „12 linii, 2-3 jeśli / else blok” nie jest trywialne. Cały kod z przynajmniej odrobiną logiki warunkowej powinien zostać przetestowany.

Z mojego doświadczenia wynika, że ​​duże refaktoryzacje nie są łatwe i rzadko działają. Jeśli nie wyznaczysz sobie precyzyjnych, drobnych celów, istnieje duże ryzyko, że rozpoczniesz niekończącą się, pociągającą za włosy przeróbkę, w której nigdy nie wylądujesz na nogach. Im większa zmiana, tym bardziej ryzykujesz zepsucie czegoś i tym więcej kłopotów będziesz miał dowiedzieć się, gdzie się nie udało.

Udoskonalanie postępów dzięki drobnym refaktoryzacjom ad hoc nie „podważa przyszłych możliwości”, umożliwia im - utrwalając bagniste podłoże, na którym leży twoja aplikacja. Zdecydowanie powinieneś to zrobić.

guillaume31
źródło
5
+1 za „testy, które piszesz testują bieżące zachowanie programu
David
17

Również ponownie: „Oryginalny kod może nie działać poprawnie” - to nie znaczy, że po prostu zmieniasz zachowanie kodu, nie martwiąc się o wpływ. Inny kod może polegać na tym, co wydaje się być uszkodzonym zachowaniem lub skutkami ubocznymi bieżącej implementacji. Pokrycie testowe istniejącej aplikacji powinno ułatwić później refaktoryzację, ponieważ pomoże ci dowiedzieć się, kiedy przypadkowo coś zepsułeś. Najpierw powinieneś przetestować najważniejsze części.

Rory Hunter
źródło
Niestety prawda. Mamy kilka oczywistych błędów, które ujawniają się w skrajnych przypadkach, których nie możemy naprawić, ponieważ nasz klient woli spójność niż poprawność. (Są one spowodowane tym, że kod gromadzący dane pozwala na to, że kod raportujący nie bierze pod uwagę, na przykład pozostawienie jednego pola w szeregu pól pustych)
Izkata
14

Odpowiedź Kiliana obejmuje najważniejsze aspekty, ale chcę rozwinąć punkty 1 i 3.

Jeśli programista chce zmienić (refaktoryzować, rozszerzyć, debugować) kod, musi to zrozumieć. Musi się upewnić, że jej zmiany wpływają dokładnie na zachowanie, którego pragnie (nic w przypadku refaktoryzacji) i nic więcej.

Jeśli są testy, ona też musi je zrozumieć. Jednocześnie testy powinny pomóc jej zrozumieć główny kod, a testy są i tak znacznie łatwiejsze do zrozumienia niż kod funkcjonalny (chyba że są to złe testy). Testy pomagają pokazać, co zmieniło się w zachowaniu starego kodu. Nawet jeśli oryginalny kod jest niepoprawny, a test sprawdza to nieprawidłowe zachowanie, nadal jest to zaletą.

Wymaga to jednak, aby testy były udokumentowane jako testowanie istniejącego zachowania, a nie specyfikacji.

Kilka przemyśleń na temat punktu 3: oprócz tego, że „wielkie uderzenie” rzadko się zdarza, jest jeszcze jedna rzecz: to wcale nie jest łatwiejsze. Aby było łatwiej, musiałoby się stosować kilka warunków:

  • Antypattern, który ma zostać zrefaktoryzowany, musi być łatwo znaleziony. Czy wszystkie twoje singletony są nazwane XYZSingleton? Czy ich instancja jest zawsze wywoływana getInstance()? Jak znaleźć swoje zbyt głębokie hierarchie? Jak szukasz swoich boskich obiektów? Wymagają one analizy metryk kodu, a następnie ręcznej kontroli metryk. Lub po prostu natkniesz się na nich podczas pracy, tak jak to robiłeś.
  • Refaktoryzacja musi być mechaniczna. W większości przypadków trudną częścią refaktoryzacji jest zrozumienie istniejącego kodu na tyle dobrze, aby wiedzieć, jak go zmienić. Znów singletony: jeśli singleton zniknie, w jaki sposób zdobędziesz wymagane informacje dla jego użytkowników? Często oznacza to zrozumienie lokalnego kalendarza, abyś wiedział, skąd uzyskać informacje. Co jest łatwiejsze: wyszukanie dziesięciu singletonów w aplikacji, zrozumienie zastosowań każdego z nich (co prowadzi do konieczności zrozumienia 60% bazy kodu) i ich rozerwanie? A może bierzesz kod, który już rozumiesz (bo nad nim pracujesz) i zgrywasz singletony, które są tam używane? Jeśli refaktoryzacja nie jest tak mechaniczna, że ​​wymaga niewielkiej wiedzy na temat otaczającego kodu lub nie wymaga jej wcale, nie ma sensu jej grupować.
  • Refaktoryzacja musi zostać zautomatyzowana. Jest to nieco oparte na opiniach, ale proszę bardzo. Trochę refaktoryzacji jest zabawne i satysfakcjonujące. Dużo refaktoryzacji jest nudne i nudne. Pozostawienie fragmentu kodu, nad którym właśnie pracowałeś, w lepszym stanie, daje przyjemne, ciepłe uczucie, zanim przejdziesz do bardziej interesujących rzeczy. Próba refaktoryzacji całej bazy kodu sprawi, że będziesz sfrustrowany i zły na idiotycznych programistów, którzy to napisali. Jeśli chcesz dokonać refaktoryzacji z dużym zamachem, musi to być w dużej mierze zautomatyzowane, aby zminimalizować frustrację. Jest to w pewnym sensie połączenie dwóch pierwszych punktów: możesz zautomatyzować refaktoryzację tylko wtedy, gdy możesz zautomatyzować znajdowanie złego kodu (tj. Łatwo go znaleźć) i zautomatyzować jego zmianę (tj. Mechaniczną).
  • Stopniowe doskonalenie zapewnia lepsze uzasadnienie biznesowe. Refaktoryzacja dużych spadków jest niezwykle destrukcyjna. Jeśli refaktoryzujesz fragment kodu, niezmiennie wchodzisz w konflikty scalania z innymi pracującymi nad nim osobami, ponieważ po prostu podzieliłeś metodę, którą zmieniali na pięć części. Po przefakturowaniu kawałka kodu o rozsądnych rozmiarach dochodzi do konfliktów z kilkoma osobami (1-2 podczas podziału megafunkcji na 600 linii, 2-4 podczas niszczenia obiektu Boga, 5 podczas wyrywania singletona z modułu ), ale i tak miałbyś takie konflikty ze względu na główne zmiany. Kiedy dokonujesz refaktoryzacji dla całego kodu, konfliktujesz ze wszystkimi. Nie wspominając o tym, że przez kilka dni wiąże kilku programistów. Stopniowe doskonalenie powoduje, że każda modyfikacja kodu trwa nieco dłużej. To sprawia, że ​​jest bardziej przewidywalny i nie ma tak widocznego okresu, w którym nic się nie dzieje poza czyszczeniem.
Sebastian Redl
źródło
12

W niektórych firmach istnieje kultura, która jest niechętna, aby umożliwić programistom ulepszenie kodu, który nie zapewnia bezpośrednio dodatkowej wartości, np. Nowej funkcjonalności.

Prawdopodobnie głoszę tutaj nawróconym, ale to wyraźnie fałszywa ekonomia. Czysty i zwięzły kod przynosi korzyści kolejnym programistom. Po prostu zwrot nie jest natychmiast widoczny.

Osobiście zgadzam się z zasadą skautową, ale inni (jak widzieliście) nie.

To powiedziawszy, oprogramowanie cierpi z powodu entropii i narasta dług techniczny. Poprzedni programiści, którzy mieli mało czasu (a może po prostu leniwi lub niedoświadczeni), mogli wdrożyć nieoptymalne rozwiązania buggy w stosunku do dobrze zaprojektowanych. Chociaż może się to wydawać pożądane, aby je refaktoryzować, ryzykujesz wprowadzenie nowych błędów w tym, co jest (dla użytkowników w każdym razie) kodem.

Niektóre zmiany wiążą się z niższym ryzykiem niż inne. Na przykład tam, gdzie pracuję, jest dużo zduplikowanego kodu, który można bezpiecznie wprowadzić do podprogramu przy minimalnym wpływie.

Ostatecznie musisz dokonać oceny, jak daleko zajdziesz do refaktoryzacji, ale dodawanie automatycznych testów, jeśli jeszcze nie istnieją, ma niezaprzeczalną wartość.

Robbie Dee
źródło
2
Zasadniczo całkowicie się zgadzam, ale w wielu firmach sprowadza się to do czasu i pieniędzy. Jeśli część „uporządkowania” zajmuje tylko kilka minut, to jest w porządku, ale gdy szacunki dotyczące uporządkowania zaczną się powiększać (dla pewnej definicji dużej), to Ty, osoba kodująca, musisz przekazać tę decyzję swojemu szefowi lub menadżer projektu. To nie twoje miejsce decyduje o wartości tego czasu. Praca nad poprawką błędów X lub nową funkcją Y może mieć znacznie wyższą wartość dla projektu / firmy / klienta.
ozz
2
Być może nie zdajesz sobie sprawy z większych problemów, takich jak złomowanie projektu w ciągu 6 miesięcy, lub po prostu, że firma bardziej ceni Twój czas (np. Robisz coś, co uważa za ważniejsze, a ktoś inny może wykonać pracę refaktoryzacyjną). Refaktoryzacja może mieć również wpływ na testy. Czy duże refaktoryzowanie spowoduje pełną regresję testu? Czy firma ma zasoby, które może wdrożyć, aby to zrobić?
ozz
Tak, jak już wspomniałeś, istnieją niezliczone powody, dla których poważna operacja na kodzie może, ale nie musi być dobrym pomysłem: inne priorytety programistyczne, czas życia oprogramowania, zasoby testowe, doświadczenie programistów, łączenie, cykl wydawania, znajomość kodu baza, dokumentacja, krytyczność misji, kultura firmy itp. itd. Jest to wezwanie do sądu
Robbie Dee
4

Z mojego doświadczenia wynika, że pewnego rodzaju test charakterystyki działa dobrze. Daje to stosunkowo szeroki, ale niezbyt konkretny zasięg testu, ale może być trudny do wdrożenia w aplikacjach GUI.

Następnie napisałbym testy jednostkowe dla części, które chcesz zmienić, i robię to za każdym razem, gdy chcesz dokonać zmiany, zwiększając w ten sposób Twój zasięg testów jednostkowych.

Takie podejście daje dobry pomysł, jeśli zmiany wpływają na inne części systemu, i pozwala nam szybciej wprowadzić wymagane zmiany.

jamesj
źródło
3

Odp: „Oryginalny kod może nie działać poprawnie”:

Testy nie są napisane w kamieniu. Można je zmienić. A jeśli testowałeś pod kątem niewłaściwej funkcji, przepisanie testu powinno być łatwiejsze. W końcu powinien się zmienić tylko oczekiwany wynik testowanej funkcji.

rem
źródło
1
IMO, poszczególne testy powinny być napisane w kamieniu, przynajmniej dopóki funkcja, którą testują, nie będzie już martwa. To one weryfikują zachowanie istniejącego systemu i pomagają zapewnić opiekunom, że ich zmiany nie złamią starszego kodu, który może już polegać na tym zachowaniu. Zmień testy funkcji na żywo, a usuniesz te zapewnienia.
cHao
3

No tak. Odpowiedź jako inżynier testowy oprogramowania. Po pierwsze i tak powinieneś przetestować wszystko, co kiedykolwiek robiłeś. Ponieważ jeśli nie, nie wiesz, czy to działa, czy nie. Może nam się to wydawać oczywiste, ale mam kolegów, którzy postrzegają to inaczej. Nawet jeśli Twój projekt jest mały, ale może nigdy nie zostać dostarczony, musisz spojrzeć użytkownikowi w twarz i powiedzieć, że wiesz, że działa, ponieważ go przetestowałeś.

Nietrywialny kod zawsze zawiera błędy (cytowanie faceta z uni; a jeśli nie ma w nim żadnych błędów, jest to trywialne), a naszym zadaniem jest ich znalezienie, zanim zrobi to klient. Stary kod zawiera starsze błędy. Jeśli oryginalny kod nie działa tak, jak powinien, chcesz o tym wiedzieć, uwierz mi. Błędy są w porządku, jeśli o nich wiesz, nie bój się ich znaleźć, po to są informacje o wydaniu.

Jeśli dobrze pamiętam, książka Refaktoryzacja mówi, aby mimo to ciągle testować. Więc jest to część procesu.

RedSonja
źródło
3

Wykonaj automatyczny zasięg testu.

Uważaj na życzenia, zarówno własne, jak i klientów i szefów. Chociaż bardzo chciałbym wierzyć, że moje zmiany będą poprawne za pierwszym razem i będę musiał przetestować tylko raz, nauczyłem się traktować takie myślenie tak samo, jak traktuję nigeryjskie e-maile oszukańcze. Cóż, głównie; Nigdy nie szukałem fałszywego e-maila, ale ostatnio (kiedy na niego krzyknąłem) zrezygnowałem z używania najlepszych praktyk. To było bolesne doświadczenie, które ciągnęło (drogo) bez końca. Nigdy więcej!

Mam ulubiony cytat z komiksu internetowego Freefall: „Czy pracowałeś kiedyś w złożonej dziedzinie, w której superwizor ma tylko ogólne pojęcie o szczegółach technicznych? ... Zatem znasz najpewniejszy sposób, aby spowodować niepowodzenie swojego superwizora wykonuj każde jego zamówienie bez pytania. ”

Prawdopodobnie właściwe jest ograniczenie czasu, który inwestujesz.

Technofil
źródło
1

Jeśli masz do czynienia z dużą ilością starszego kodu, który nie jest obecnie testowany, dobrym pomysłem jest uzyskanie zasięgu testowego teraz zamiast czekania na hipotetyczny duży przepis w przyszłości. Rozpoczęcie od napisania testów jednostkowych nie jest.

Bez automatycznego testowania po wprowadzeniu jakichkolwiek zmian w kodzie należy wykonać ręczne testowanie aplikacji od końca do końca, aby upewnić się, że działa. Zacznij od napisania testów integracji wysokiego poziomu, aby to zastąpić. Jeśli Twoja aplikacja wczytuje pliki, sprawdza je, przetwarza dane w określony sposób i wyświetla wyniki, które chcesz przechwycić.

Idealnie będziesz mieć dane z ręcznego planu testów lub będziesz w stanie uzyskać próbkę rzeczywistych danych produkcyjnych do wykorzystania. Jeśli nie, ponieważ aplikacja jest produkowana, w większości przypadków robi to, co powinna, więc po prostu wymyśl dane, które osiągną wszystkie najwyższe punkty i założymy, że dane wyjściowe są prawidłowe. Nie jest to gorsze niż przyjmowanie małej funkcji, zakładanie, że robi to, co jej nazwa lub jakiekolwiek komentarze sugerują, że powinna działać, i pisanie testów, zakładając, że działa poprawnie.

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

Gdy masz już wystarczająco dużo testów wysokiego poziomu, aby uchwycić normalne działanie aplikacji i najczęstsze przypadki błędów, czas poświęcony na uderzenie w klawiaturę, aby spróbować wykryć błędy z kodu, robiąc coś innego niż myślałeś, że to powinno znacznie spaść, co znacznie ułatwi refaktoryzację w przyszłości (lub nawet duże przepisanie).

Ponieważ możesz rozszerzyć zakres testów jednostkowych, możesz ograniczyć lub nawet wycofać większość testów integracyjnych. Jeśli aplikacja odczytuje / zapisuje pliki lub uzyskuje dostęp do bazy danych, oczywistym miejscem do rozpoczęcia jest przetestowanie tych części w izolacji i wyszydzenie ich lub rozpoczęcie testów od utworzenia struktur danych odczytanych z pliku / bazy danych. W rzeczywistości stworzenie tej infrastruktury testowej zajmie dużo więcej czasu niż napisanie zestawu szybkich i brudnych testów; i za każdym razem, gdy przeprowadzasz 2-minutowy zestaw testów integracyjnych zamiast 30 minut ręcznego testowania ułamka tego, co obejmowały testy integracyjne, już osiągasz dużą wygraną.

Dan Neely
źródło