3 sprawdzone sposoby na wprowadzanie testów do istniejącego kodu

15

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!

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

15 Comments

  1. Tak proste i zarazem tak trudne do wdrożenia. Jeden z ostatnich fragmentów ‘Kilka dłuższych chwil (lat?) potrwa, ale nie da się wieloletnich zaniechań naprawić – ot tak – w miesiąc.’ zawiera dla mnie całą esencję tego tekstu, zapiszę go sobie chyba na mojej tablicy żebym cały czas miał go przed oczami :)

  2. WhiteLightning on

    A co zrobic jesli systemm jest w technologi gdzie nie za bardzo jest jak testy wpiac? (xsl wywolywany z jsp)

    • Whitelightning,
      Logikę wyrzucasz poza aplikację, o tym pisałem w poprzednim poście odnośnie testowania (podlinkowany jest na początku tego tekstu). I taką logikę oderwaną od aplikacji testujesz.

  3. Sposób numer 2 z tworzeniem testów przy okazji poprawiania bugów wydaje mi się najprostszy do wdrożenia. Ma swoje uzasadnienie biznesowe i da się w ten sposób znaleźć “budźet” na taki cel. Choć i tutaj często może dojść do sytuacji, że naprawa buga zajmie nam godzine, a napisanie testu, który będzie wymagał refaktoryzacji kodu pochłonie nam 5 godzin. A jak wielka frustracja może nam przy okazji towarzyszyć to już inna bajka. I z tego powodu w moim odczuciu najbardziej komfortowym wyjściem dla programisty będzie pierwszy sposób – pisanie testów tylko do nowego kodu. Tworzymy w ten sposób system lepszym, przejrzystszym, a przy okazji mamy uczucie zrobienia dobrej roboty.

    • Paweł,
      Zgadzam się, choć przy bugach to nie frustracja – to poczucie wykonania świetnej roboty. Nie dość że bug poprawiony, nie dość że z gwarancją “on tutaj nigdy więcej nie wystąpi” to jeszcze pokonałeś stary słaby, nietestowalny wcześniej kod.

  4. Pingback: dotnetomaniak.pl

  5. Jeżeli mamy funkcjonalność, dla testowania której, musimy zmienić kod, a kod “mniej więcej” działa i niebezpiecznie jest go ot tak zmienić bez żadnych bug fixów, czy dodatkowego budżetu – warto zastanowić się nad testami automatycznymi, np. Selenium. Wiadomo, że to jest work around, ale zawsze lepiej mieć testy UI sprawdzajace np. scenariusze wyliczania cen , niż nie mieć ich w ogóle ;)

  6. Ja mam dwa doświadczenia (w tym są zawarte i moje) dlaczego ludzie nie piszą testów:

    1. baza danych – pod spodem zawsze gdzieś jest jakaś baza danych. Większość używa ORMów (EF, NH) i to wszystko trzeba konfigurować pod testy… bleeee
    2. (ogólnie) brak dobrej separacji logiki domenowej od infrastruktury, itp, itd.

    Napisanie testów (podejście TDD) jest banalnie proste do przykładów typu ICalculator. Testy w prawdziwym życiu (dla wielu) to inna bajka.

    • Michał Lewandowski on

      Ad 1: od siebie mogę dodać, że w najnowszym entity framework core testowanie to bajka – można sobie spokojnie skonfigurować kontekst podając w konstruktorze opcje. Jedna z nich umożliwia podmianę źródła danych na Memory, przez co po prostu ładujemy dane do pamięci i dzięki temu testy działają od razu po wyjęciu z pudełka, tj pobrania projektu z repozytorium :)

  7. Dariol pisze o logice domenowej. Mały OT w sprawie DDD do praktyków.
    Jak przechowujecie dane dla różnych agregatów, które zawierają “te same” encje?
    Np Zamówienie w Handlu i Produkcji. Jakaś pani z MS na prezentacji mówi nawet o osobnych bazach danych lub osobnych przestrzeniach nazw. Dla mnie to mentalnie trudne do przyjęcia.

    • Sprawa osobnych baz danych tyczy sie osobnych Bounded Context (BC). Dane z roznych agregatow nalezacych do jednego BC moga byc zapisane w te same struktury jednej bazy danych. Odpowiedzialnosc za to biora repozytoria.

      Jesli Handel i Produkcja to osobne BC, wtedy masz kilka opcji. Mozesz uzywac roznych baz danych – synchronizacja przedbiega jako rekacja na Eventy. Nikt nie zabrania tez uzywania jednej bazy dla wielu BC. Jak zwykle wszystko zalezy od potrzeb i jak bardzo rozne BC maja byc “bounded” :-)

  8. Pingback: Refactoring sprint? Plażo, please... | devstyle.pl | Maciej Aniserowicz

Newsletter: devstyle weekly!
Dołącz do 1000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania w najbliższy piątek!
Niech DEV będzie z Tobą!