Tworzenie nowego kodu jest fajne. Dlaczego? Bo możemy najpierw napisać do niego testy. O tym, jak się tego nauczyć, poczytasz w tym niedawnym tekście. Ale co jeśli już MASZ kod? Michę pełną spaghetti?
Do problemu tego można podejść na kilka sposobów. Jeden jest bezsensowny, pozostałe: działają.
AAAAAATTTAAAACKK!!!!
Rzucanie się “na hurra” i dopisywanie testów tylko po co, żeby “mieć testy”, to złe rozwiązanie. Choć wcale nierzadko spotykane.
Pracowałem kiedyś przy dużym projekcie w kilkunastoosobowym zespole. Kod już wtedy był dość wiekowy i niejedna łza kapnęła na klawiaturę podczas prób poprawienia jego jakości. Jak można ulepszyć kod, który nie ma testów? Hmm… dopisać do niego testy, co nie? Niby logiczne, ale nie do końca.
Ówczesna próba poradzenia z problemem wyglądała tak: bierzemy na wakacje dwóch praktykantów i oni doklepią testy do systemu! Ile można napisać testów przez dwa miesiące? Całą masę, oczywiście. No i napisali. Po czym odeszli, a my te testy skasowaliśmy, bo się do niczego nie nadawały. Bummer.
Testowanie istniejącego kodu “na siłę” jest bez sensu.
Mając JAKIŚ kod, który jest używany, który MNIEJ WIĘCEJ działa i – co istotne – zarabia, nie możemy nagle rzucić wszystkiego i zacząć go “otestowywać” na siłę. Nie da się dopisać testów, nie ingerując w jego flaki. Bo taki kod w swojej oryginalnej postaci prawie na pewno do testowania się po prostu nie nadaje. Szkoda na to czasu, szkoda ludzi. A korzyści będą raczej minimalne, albo nie będzie ich wcale.
Często takie podejście jest stosowane ze złych pobudek:
Code coverage
Metryka wskazująca “ile linijek kodu jest wykonywanych w kontekście testów” mówi dość niewiele. Szczególnie w dużych, nieotestowanych systemach, nie ma sensu dążyć do wysokich wartości tego współczynnika w krótkim czasie. Podążanie za radami “cały kod trzeba pokryć testami” może wydawać się sensowne, ale niezrozumienie implikacji doprowadzi do problemów.
Gdzieś kiedyś zasłyszałem ciekawe stwierdzenie:
Code coverage może przyjąć dwie istotne wartości.
0% mówi, że testujesz za mało. 100% mówi, że testujesz za dużo.
Jak dla mnie – robi to sens, masę sensu. Nie zawracajmy sobie głowy pokryciem kodu na tym etapie.
Nowy kod
Kiedy zatem pisać testy? Powtórzę jak mantrę: przed napisaniem kodu produkcyjnego! Bo przecież do istniejących systemów dopisuje się nowe rzeczy, prawda?
Przed dopisaniem nowego kodu napisz do niego testy. Czyli: test-first approach.
Wystarczy uświadomić sobie, że każda taka, malutka nawet, nowość, może być tworzona w oderwaniu od całego tego syfu. Piszmy małe komponenty, definiujące swoje parametry wejściowe oraz wartości zwracane. Niech ten wielki, zły, stary system się z nimi za pomocą jakichś adapterów zintegruje. Pisałem o tym już parę lat temu tutaj, nazywając ów koncept mikro-kontraktami. A każdy taki mikro-kontrakt niech powstaje zgodnie z “test-first approach“.
Proste? Może się na pierwszy rzut oka nie wydaje, ale… tak, to jest proste.
Bugi
Drugi scenariusz dopisywania testów do istniejącego systemu to kontekst poprawiania błędów. Bo błędy są przecież zgłaszane.
Przed naprawieniem błędu udowodnij testem, że błąd faktycznie występuje.
PRZED naprawieniem błędu powinno się poświęcić trochę czasu na udowodnienie (w teście!), że ten błąd faktycznie występuje. Czyli najpierw piszemy “czerwony”, “nieprzechodzący” test, modelujący poprawne zachowanie. A dopiero kod, dzięki któremu test się zieleni. Mi bardzo podoba się nawet praktyka wrzucania takich testów i poprawek do osobnych commitów.
Owszem, może to wymagać trochę czasu i refactoringu w poprawianym obszarze, ale przecież i tak będziemy ten kod zmieniać. Natomiast mając test upewniamy się, że błąd jest faktycznie naprawiony. I – co niezmiernie istotne – nie wystąpi ponownie.
Refactoring
Zasadność dedykowania czasu wyłącznie “na refactoring” póki co odłożymy na bok. Zajmiemy się tym innym razem (EDIT: tutaj pojawił się post na ten temat). Póki co: założymy, że zespoły są skłonne negocjować godziny, dni czy nawet całe sprinty na “poprawę kodu”, bez dodawania nowych funkcji i bez poprawiania błędów.
Na czym polega refactoring? Na zmianie struktury kodu, bez modyfikowania jego zachowania. A jak upewnić się, że faktycznie niczego po drodze nie zepsujemy? Hmm…
Przed refactoringiem upewnij się, że niczego nie zepsujesz. Czyli: napisz testy.
Odpowiednio napisane testy do dobrze wyselekcjowanego obszaru kodu uchronią przed tzw. błędami regresji. W testach modelujemy aktualne zachowanie pewnej logiki i dzięki temu gwarantujemy, że refactoring niczego nie zepsuje.
Niespiesznie, powoli, jak żółw ociężale…
Jeden teścik tu, jeden tam. Czas płynie. System ewoluuje. Zespół się uczy. A dzięki tym praktykom jakość kodu autentycznie zacznie stale wzrastać:
- testuj przed napisaniem nowego kodu
- testuj przed poprawieniem buga
- testuj przed refactoringiem
To działa. Kilka dłuższych chwil (lat?) potrwa, ale:
Nie da się wieloletnich zaniechań naprawić – ot tak – w miesiąc.
O czym zresztą niedawno rozmawiałem z Jarkiem Pałką w DevTalk o Legacy Code.
Przeczytaj. Posłuchaj. I do dzieła. Powodzenia!
Uwaga! Z wielką przyjemnością ogłaszam Inicjatywę SmartTesting.pl i serdecznie Cię na nią zapraszam! Tam wraz z TOP Expertami będziemy dostarczać masę rewelacyjnych materiałów o testach!