Refaktoryzacja - czy właściwe jest po prostu przepisanie kodu, o ile wszystkie testy przejdą pomyślnie?

9

Niedawno obejrzałem „Wszystkie małe rzeczy” z RailsConf 2014. Podczas tej rozmowy Sandi Metz refaktoryzuje funkcję, która zawiera dużą zagnieżdżoną instrukcję if:

def tick
    if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
        if @quality > 0
            if @name != 'Sulfuras, Hand of Ragnaros'
                @quality -= 1
            end
        end
    else
        ...
    end
    ...
end

Pierwszym krokiem jest podzielenie funkcji na kilka mniejszych:

def tick
    case name
    when 'Aged Brie'
        return brie_tick
    ...
    end
end

def brie_tick
    @days_remaining -= 1
    return if quality >= 50

    @quality += 1
    @quality += 1 if @days_remaining <= 0
end

Dla mnie interesujący był sposób pisania tych mniejszych funkcji. brie_tick, na przykład, nie został napisany przez wyodrębnienie odpowiednich części oryginalnej tickfunkcji, ale od zera przez odniesienie do test_brie_*testów jednostkowych. Po przejściu wszystkich tych testów jednostkowychbrie_tick uznano zostały wykonane. Po wykonaniu wszystkich małych funkcji oryginalna tickfunkcja monolityczna została usunięta.

Niestety prezenter wydawał się nieświadomy, że takie podejście doprowadziło do tego, że trzy z czterech *_tickfunkcji były nieprawidłowe (a druga była pusta!). Istnieją przypadki skrajne, w których zachowanie *_tickfunkcji różni się od tickfunkcji oryginalnej . Na przykład, @days_remaining <= 0w brie_tickpowinny być < 0- tak brie_ticknie działa prawidłowo, gdy nazywa się days_remaining == 1iquality < 50 .

Co tu poszło nie tak? Czy to błąd testowania - ponieważ nie było testów dla tych konkretnych przypadków skrajnych? A może błąd refaktoryzacji - ponieważ kod powinien zostać przetworzony krok po kroku, a nie przepisany od nowa?

użytkownik200783
źródło
2
Nie jestem pewien, czy dostaję pytanie. Oczywiście przepisanie kodu jest w porządku. Nie jestem pewien, co masz na myśli mówiąc „czy można po prostu przepisać kod”. Jeśli pytasz „Czy można przepisać kod bez zastanowienia się nad nim”, odpowiedź brzmi „nie”, podobnie jak pisanie kodu w ten sposób nie jest w porządku .
John Wu
Dzieje się tak często ze względu na plany testowe skoncentrowane głównie na testowaniu przypadków sukcesu, a bardzo niewiele (lub wcale) na przypadkach błędów lub przypadków niepełnego wykorzystania. Jest to głównie wyciek zasięgu. Wyciek testowania.
Laiv
@JohnWu - miałem wrażenie, że refaktoryzacja była generalnie dokonywana jako seria małych przekształceń kodu źródłowego („metoda wyodrębniania” itp.), A nie po prostu przez przepisanie kodu (przez co mam na myśli napisanie go od nowa bez podstaw patrząc na istniejący kod, jak zrobiono w połączonej prezentacji).
user200783
@JohnWu - Czy przepisywanie od zera jest akceptowalną techniką refaktoryzacji? Jeśli nie, to rozczarowujące, że tak dobrze przemyślana prezentacja na temat refaktoryzacji przyjmuje takie podejście. OTOH, jeśli jest to akceptowalne, to można za winę za niezamierzone testy obwinić niezamierzone zmiany zachowania - ale czy jest jakiś sposób, aby mieć pewność, że testy obejmują wszystkie możliwe przypadki skrajne?
user200783
@ User200783 Cóż, to jest większe pytanie, prawda? (W jaki sposób mam zagwarantować, że moje testy są kompleksowe?) Pragmatycznie, prawdopodobnie wykonałbym raport o pokryciu kodu przed wprowadzeniem jakichkolwiek zmian i dokładnie zbadałbym wszystkie obszary kodu, które nie ćwicz się, upewniając się, że zespół programistów pamięta o nich podczas przepisywania logiki.
John Wu

Odpowiedzi:

11

Czy to błąd testowania - ponieważ nie było testów dla tych konkretnych przypadków skrajnych? A może błąd refaktoryzacji - ponieważ kod powinien zostać przetworzony krok po kroku, a nie przepisany od nowa?

Obie. Refaktoryzacja przy użyciu tylko standardowych kroków z oryginalnej książki Fowlers jest zdecydowanie mniej podatna na błędy niż przepisywanie, więc często lepiej jest używać tylko tego rodzaju kroków dla dzieci. Nawet jeśli nie ma testów jednostkowych dla każdego przypadku na krawędzi, a nawet jeśli środowisko nie zapewnia automatycznego refaktoryzacji, pojedyncza zmiana kodu, taka jak „wprowadzanie zmiennej wyjaśniającej” lub „funkcja wyodrębniania”, ma znacznie mniejszą szansę na zmianę szczegółów zachowania istniejący kod niż całkowite przepisanie funkcji.

Czasami jednak przepisanie fragmentu kodu jest tym, czego potrzebujesz lub chcesz zrobić. A jeśli tak jest, potrzebujesz lepszych testów.

Należy pamiętać, że nawet podczas korzystania z narzędzia do refaktoryzacji zawsze istnieje pewne ryzyko wprowadzenia błędów podczas zmiany kodu, niezależnie od zastosowania mniejszych lub większych kroków. Dlatego refaktoryzacja zawsze wymaga testów. Zauważ też, że testy mogą tylko zmniejszyć prawdopodobieństwo błędów, ale nigdy nie dowodzą ich braku - jednak użycie technik takich jak przeglądanie kodu i zasięgu gałęzi może dać ci wysoki poziom pewności, aw przypadku przepisania sekcji kodu jest to często warto stosować takie techniki.

Doktor Brown
źródło
1
Dzięki, to ma sens. Tak więc, jeśli ostatecznym rozwiązaniem niepożądanych zmian w zachowaniu są kompleksowe testy, czy istnieje sposób, aby mieć pewność, że testy obejmują wszystkie możliwe przypadki skrajne? Na przykład, możliwe byłoby uzyskanie 100% pokrycia, brie_tickprzy jednoczesnym nigdy nie testowaniu problematycznego @days_remaining == 1przypadku, na przykład przez testowanie z @days_remainingustawieniem na 10i -10.
user200783
2
Nigdy nie możesz być absolutnie pewien, że testy obejmują wszystkie możliwe przypadki krawędzi, ponieważ nie jest możliwe przetestowanie przy użyciu wszystkich możliwych danych wejściowych. Ale istnieje wiele sposobów na uzyskanie większej pewności siebie w testach. Możesz spojrzeć na testy mutacji , które są sposobem na przetestowanie skuteczności testów.
bdsl
1
W tym przypadku pominięte gałęzie mogły zostać przechwycone za pomocą narzędzia do pokrywania kodu podczas opracowywania testów.
cbojar
2

Co tu poszło nie tak? Czy to błąd testowania - ponieważ nie było testów dla tych konkretnych przypadków skrajnych? A może błąd refaktoryzacji - ponieważ kod powinien zostać przetworzony krok po kroku, a nie przepisany od nowa?

Jedną z rzeczy, która jest naprawdę trudna w pracy ze starszym kodem: uzyskanie pełnego zrozumienia obecnego zachowania.

Starszy kod bez testów ograniczających wszystkie zachowania to powszechnym wzorcem na wolności. Co sprawia, że ​​zgadujesz: czy to oznacza, że ​​nieograniczone zachowania są zmiennymi swobodnymi? lub wymagania, które są nieokreślone?

Z rozmowy :

Teraz jest to prawdziwe refaktoryzacja zgodnie z definicją refaktoryzacji; Zamierzam refaktoryzować ten kod. Zamierzam zmienić jego układ bez zmiany jego zachowania.

To jest bardziej konserwatywne podejście; jeśli wymagania mogą być nieokreślone, jeśli testy nie wychwytują całej istniejącej logiki, musisz być bardzo bardzo ostrożny z tym, jak postępujesz.

Na pewno możesz stwierdzić, że jeśli testy nieadekwatnie opisują zachowanie systemu, to oznacza to „niepowodzenie testowania”. I myślę, że to sprawiedliwe - ale w rzeczywistości nie przydatne; jest to powszechny problem występujący na wolności.

A może błąd refaktoryzacji - ponieważ kod powinien zostać przetworzony krok po kroku, a nie przepisany od nowa?

Problem nie jest całkiem , że przemiany powinny być krok po kroku; ale raczej to, że wybór narzędzia refaktoryzacji (operator klawiatury ludzkiej? zamiast automatyzacji sterowanej) nie był dobrze dopasowany do zasięgu testu, z powodu wyższego poziomu błędów.

Można temu zaradzić albo przez zastosowanie narzędzi refaktoryzujących o wyższej niezawodności, albo przez wprowadzenie szerszej baterii testów w celu poprawy ograniczeń systemu.

Myślę więc, że twoje połączenie jest źle wybrane; ANDNie OR.

VoiceOfUnreason
źródło
2

Refaktoryzacja nie powinna zmieniać zachowania kodu widocznego z zewnątrz. To jest cel.

Jeśli testy jednostkowe nie powiodą się, oznacza to zmianę zachowania. Ale zdawanie testów jednostkowych nigdy nie jest celem. Pomaga mniej więcej osiągnąć swój cel. Jeśli refaktoryzacja zmieni zachowanie widoczne z zewnątrz, a wszystkie testy jednostkowe zakończą się pomyślnie, wówczas refaktoryzacja nie powiodła się.

Testy jednostki roboczej w tym przypadku dają tylko złe poczucie sukcesu. Ale co poszło nie tak? Dwie rzeczy: refaktoryzacja była nieostrożna, a testy jednostkowe nie były bardzo dobre.

gnasher729
źródło
1

Jeśli zdefiniujesz „poprawny” jako „test pozytywny”, to z definicji nie jest błędem zmiana zachowania niesprawdzonego.

Jeśli określone zachowanie krawędzi powinno zostać zdefiniowane, dodaj test, jeśli nie, to nie ma znaczenia, co się stanie. Jeśli jesteś naprawdę pedantyczny, możesz napisać test, który sprawdza, truekiedy w tym przypadku krawędzi, aby udokumentować, że nie obchodzi Cię, jakie jest zachowanie.

Caleth
źródło