Test-Driven Development jest jedną z tych praktyk, które pomimo tego, że są w świadomości programistów od bardzo dawna, nie są szeroko stosowane. Znam wiele osób, które w trakcie rozmowy zgadzają się ze wszystkimi korzyściami, które niesie ze sobą TDD, a mimo to, Ci sami ludzie często nie praktykują TDD twierdząc, że „u mnie w projekcie to nie działa”.
Najczęściej jednak problem nie leży w specyficznym projekcie. Zazwyczaj wynika on z braku doświadczenia i umiejętności w korzystaniu z TDD. Jednym z częstych błędów popełnianych przez osoby zaczynające swoją przygodę z Test-Driven Development jest chęć wykorzystania tej techniki do zaimplementowania każdej nowej klasy czy też metody. W tym artykule wytłumaczę, dlaczego jest to zły pomysł i co można zrobić, aby się przed tym błędem uchronić. Jeżeli chcesz dowiedzieć się więcej na ten temat zapoznaj się również z kursem Masterclass Clean Architecture.
Na czym polega Test-Driven Development?
Test-Driven Development jest techniką pomagającą tworzyć kod produkcyjny, z którego łatwo korzystać i który nie posiada nadmiarowej implementacji. Ważne jest, aby pamiętać, że TDD nie jest techniką, której celem jest tworzenie dobrych testów (chociaż jest to z pewnością pozytywny efekt uboczny). Testy są narzędziem pomagającym pisać lepszy kod produkcyjny.
Cała idea TDD sprowadza się do tego, aby cykl dodawania kodu rozbić na trzy fazy:
- Red – tworzymy test, który się uruchamia, ale nie przechodzi.
- Green – dodajemy kod, który sprawia, że odpalenie testów kończy się sukcesem.
- Refactoring – poprawa jakości napisanego kodu.
![obraz119.webp](/static/c4b66e5aae09bf7d0ae5dd854a3b8497/3975d/obraz119_85356b4de6.webp)
Jak powstaje problem?
Spójrzmy na przykład stosowania Test-Driven Development:
- Rozpoczynamy od stworzenia pierwszego scenariusza testowego, który uruchamiamy, a który nie przechodzi:
![obraz2.webp](/static/db1b1e7fcdd95cad8d51e371c7b17968/34dd4/obraz2_7dd34ab603.webp)
- Następnie dodajemy kod, który spełnia wymagania zawarte w pierwszym teście:
![obraz3.webp](/static/abce41c92878f6eef0e07850c79fe85c/3975d/obraz3_fe6ab39090.webp)
- Kolejnym krokiem jest refaktoryzacja testów oraz/lub kodu produkcyjnego jeżeli jest taka potrzeba:
![obraz4.webp](/static/c55de1ac307f38857735d823fbdc1611/bd0e1/obraz4_d8858fb50f.webp)
- Dodajemy kolejny scenariusz testowy:
![obraz5.webp](/static/1be6804d9d0a0bb7d8d964b44f1cac03/3975d/obraz5_8217ddc9e2.webp)
- Dodajemy kod spełniający nowe wymagania:
![obraz6.webp](/static/ebd8d72f11457b5ab1ed896f94465e87/3975d/obraz6_9826691d98.webp)
- Kolejna refaktoryzacja:
![obraz7.webp](/static/42f7bdc40d0c7331e906b5505fd879ef/3a6cc/obraz7_e4dc07314b.webp)
Jak widać, podczas refaktoryzacji wydzieliliśmy z naszej klasy SystemUnderTests (SUT) dwie zależności. To jest właśnie moment, w którym wielu programistów popełnia błąd – zaczynają dodawać testy do naszych nowo powstałych zależności. Co więcej, często zdarza się, że starają się to zrobić wykorzystując Test-Driven Development.
![obraz8.webp](/static/243274b1765adba0d1df60a7e0b7b1cc/c74d8/obraz8_c951530e28.webp)
Takie postępowanie często sprawiają, że programiści porzucają TDD ze względu na zbyt duży koszt i ciężar, który ze sobą niesie. I szczerze mówiąc, mógłbym się z tym zgodzić gdyby nie to, że … nie ma to nic wspólnego Test-Driven Development.
Dlaczego to jest złe?
Na początku jeszcze raz powtórzę, że dopisanie testów do nowo wydzielonych zależności nie jest w żaden sposób powiązane z TDD. Nie w taki sposób działa ta technika. Dlaczego jednak zrobienie tej przerwy jest takie problematyczne?
-
Scenariusze testowe, które dodajemy w krokach Red, mają na celu implementację funkcjonalności, która realizuje nowe wymagania. To jest główny powód, dla którego SUT w ogóle powstaje i jest rozwijany podczas kolejnych cykli. Jeżeli zaczynamy pisać testy do zależności, które powstały w wyniku refaktoryzacji i zaczynamy je rozwijać korzystając z TDD może okazać się, że dodamy tam funkcjonalność, która tylko wydaje się przydatna później.
-
Dodając testy do każdej zależności, musimy oderwać się od dodawania kolejnych scenariuszy testowych mających na celu rozwój głównej funkcjonalności (SUT), a co za tym idzie, spowalniamy sam proces realizacji tych wymagań.
-
W kolejnych cyklach, dodając większą ilość scenariuszy i modyfikując implementację SUT możemy dojść do wniosku, że wcześniejsze refaktoryzacje (wydzielenie klas Dependency1 oraz Dependency2) jednak nie są potrzebne i można się ich pozbyć lub zmodyfikować. Dopóki nie skończysz implementacji SUT struktura klas może się zmieniać wielokrotnie. Jeżeli tak się stanie, to wysiłek włożony w testy zależności zostanie zmarnowany.
Jak robić to dobrze?
- Skup się na rozwoju SUT – funkcjonalność, która ma być za pomocą SUT dostarczona jest naszym głównym zadaniem.
- Nie trać swojego czasu na poprawianie i rozwój zależności, które jeszcze mogą się zmienić.
- Nie trać swojego czasu na testowanie zależności, które mogą zniknąć zaraz po tym jak dodasz kolejny scenariusz testowy.
- Nie przestawaj rozwijać SUT aż do momentu kiedy jesteś w stanie dodać kolejny test, który nie przejdzie. Dopiero gdy nie znajdziesz takiego testu, możesz skupić się na rozwoju i testach wydzielonych zależności.
![obraz9.webp](/static/e22174663885bef96b06687cb315a426/77fd7/obraz9_e62ed2f7ca.webp)
Dlatego też naszym kolejnym krokiem nie powinno być dodawanie testów do Dependency1 i Dependency2 tylko dodanie kolejnego scenariusza testowego naszej funkcjonalności (SUT):
![obraz10.webp](/static/eefc461f93031f16cdc2cd34030be4ca/a8b7a/obraz10_d9ce603454.webp)
TDD to nie koniec
Oczywiście nie oznacza to, że testy klas Dependency1 oraz Dependency2 nie zostaną dodane. Prawda jest taka, że po skończeniu TDD, gdy nie będziesz w stanie dodać żadnego nowego scenariusza, który nie przechodzi, możesz nadal dostrzec miejsca/scenariusze, które warto dodatkowo zweryfikować:
- Możliwe, że będziesz chciał dodać testy do zależności, które powstały podczas refaktoryzacji.
- Czasami istnieje potrzeba dodania testów, gdzie dane wejściowe są bardziej złożone i skomplikowane niż te wykorzystane podczas kroków TDD.
Podsumowanie
Test-Driven Development to technika rozwoju kodu produkcyjnego, a nie pisania testów. To oznacza, że po skończeniu TDD mogą istnieć jeszcze elementy kodu, które będziecie chcieli zweryfikować. Należy również pamiętać, że tę technikę najczęściej stosujemy, dodając nowe funkcjonalności i to właśnie o kształt i implementacje tych funkcjonalności (niezależnie od tego z ilu elementów się składają) TDD pomaga nam zadbać. To właśnie funkcjonalność, a nie każda pojedyncza klasa jest miejscem, gdzie wykorzystanie Test-Driven Development przynosi nam więcej pożytku, niż kosztuje wysiłku.
Chcesz poszerzyć swoją wiedzę?
Sprawdź kurs Masterclass Clean Architecture. Kurs omawia wykorzystanie dobrych praktyk związanych z architekturą, jakością oprogramowania oraz jego utrzymywaniem. Podczas kursu zapoznasz się z teorią, najczęstszymi problemami oraz praktycznym zastosowaniem wzorców/praktyk/technik takich jak architektura hexagonalna, CQRS, test-driven development, domain-driven design i wiele innych. Kurs NIE MA na celu kompleksowego omówienia każdej z technik, a pokazanie ich praktycznego zastosowania w codziennym rozwoju aplikacji.
![obraz12.webp](/static/cbbea5484ea6b9f17783563b43e91ceb/112d9/obraz12_95536bbd94.webp)