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 tick
funkcji, 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 tick
funkcja monolityczna została usunięta.
Niestety prezenter wydawał się nieświadomy, że takie podejście doprowadziło do tego, że trzy z czterech *_tick
funkcji były nieprawidłowe (a druga była pusta!). Istnieją przypadki skrajne, w których zachowanie *_tick
funkcji różni się od tick
funkcji oryginalnej . Na przykład, @days_remaining <= 0
w brie_tick
powinny być < 0
- tak brie_tick
nie działa prawidłowo, gdy nazywa się days_remaining == 1
iquality < 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?
źródło
Odpowiedzi:
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.
źródło
brie_tick
przy jednoczesnym nigdy nie testowaniu problematycznego@days_remaining == 1
przypadku, na przykład przez testowanie z@days_remaining
ustawieniem na10
i-10
.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 :
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.
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;
AND
NieOR
.źródło
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.
źródło
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,
true
kiedy w tym przypadku krawędzi, aby udokumentować, że nie obchodzi Cię, jakie jest zachowanie.źródło