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.
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:
- Następnie dodajemy kod, który spełnia wymagania zawarte w pierwszym teście:
- Kolejnym krokiem jest refaktoryzacja testów oraz/lub kodu produkcyjnego jeżeli jest taka potrzeba:
- Dodajemy kolejny scenariusz testowy:
- Dodajemy kod spełniający nowe wymagania:
- Kolejna refaktoryzacja:
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.
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.
Dlatego też naszym kolejnym krokiem nie powinno być dodawanie testów do Dependency1 i Dependency2 tylko dodanie kolejnego scenariusza testowego naszej funkcjonalności (SUT):
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.