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 static
metod 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ę?
Odpowiedzi:
Oto moje osobiste nienaukowe wrażenie: wszystkie trzy powody brzmią jak rozpowszechnione, ale fałszywe złudzenia poznawcze.
źródło
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ć.
źródło
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.
źródło
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:
XYZSingleton
? Czy ich instancja jest zawsze wywoływanagetInstance()
? 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ś.źródło
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ść.
źródło
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.
źródło
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.
źródło
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.
źródło
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.
źródło
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.
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ą.
źródło