AntyPatternCzęść NR.8 W tym wpisie omówimy antywzorce TDD oraz jego ograniczenia.

Pokazałem ci przykład pracy w metodyce TDD, używając ASP.NET CORE i Entity Framework. Pokazałem Ci jak z AutoFixture, możesz zautomatyzować pewne czynności w testach. Jeśli czytałeś poprzednie wpisy, to wiesz, jak ważne jest wstrzykiwanie zależności i rozbijanie kodu na interfejsy.

Na początku szybko omówiłem Ci zasadę RED-GREEN-REFACTOR. Chciałem, abyś jak najszybciej mógł wskoczyć w TDD, bez długiego nawijania, o co chodzi. Dziś omówimy, jednak to, co powinno być omówione na samym początku, czyli JAK dobrze pisać testy jednostkowe.

Zapraszam :)

Anty-wzorce w testach: zależność pomiędzy testami

Najważniejsza zasada w testach to UNIKANIE za wszelką cenne zależności pomiędzy testami. 

Pamiętasz jak w warstwie dostępu do danych, w każdym teście tworzyłem nową bazę danych w pamięci RAM. Robiłem to po to, aby jeden test nie miał wpływu na drugi.

Pamiętaj:

  • Kolejność wykonywania testów nie powinna mieć znaczenia. Wszystkie testy powinny działać tak samo, gdy kod źródłowy się nie zmieniał.
    • Jeśli tak nie jest to masz testy zależne od siebie
    • Jeśli będzie miał testy powiązane, to później będziesz miał  albo czerwone testy znikąd lub fałszywe pozytywy. Czyli takie testy będą gówno warte.
  • Warto też zwrócić uwagę czy twój framework wykonuje testy po kolei, czy wykonuje je współbieżnie w kilku wątkach.
    • O ile testy wykonywane asynchroniczne wykonają się szybciej, to zawsze możesz napotkać problemy, gdy testy korzystają z tych samych zmiennych globalnych.
    • Domyślnie w frameworkach .NET jak XUnit testy są wykonywane po kolei. Moim zdaniem lepiej by testy wykonały się wolniej, niż miały one mieć dodatkowy kod blokujący działanie wielowątkowe.

Anty-wzorce w testach: Testowanie detali implementacyjnych

Testy powinny się koncentrować na tym "co" robimy, ale nie na tym "jak to robimy".

Nie powiem czasem, ta zasada jest trudna to pilnowania. Jak pamiętasz w testach kontrolerów ASP.NET CORE, musiałem w którymś momencie testu określić, czym dokładnie jest IActionResult, aby sprawdzić przekierowanie do widoku, jak i sam Model.

Czy ja w taki wypadku testuje "co" ma robić kontroler pod wpływem pewnych zdarzeń, czy już powoli sprawdzać "jak" o to robi?

Gdy piszesz testy, musisz ograniczyć do minimum szczegóły implementacyjne. 

Jeżeli w twoim teście jest bardzo dużo MOCK-ów, którym mówisz, co ma się dziać dokładnie, to istnieje duża szansa, że testujesz "Jak coś działa", ale nie "co" i jakie mam rezultaty danych zdarzeń.

Dlaczego ta zasada jest tak ważna?

Im więcej będziesz miał szczegółów implementacyjnych w kodzie, tym bardziej twoje testy będą delikatniejsze na rozwalenie.

W kodzie będziesz nie raz zmieniał "JAK" coś działa i nie chciałbyś za każdym razem zmieniać potem kod testowy, aby on dostosował się do nowego JAK. Wydaje się to głupie, gdy definicja specyfikacji danego testu się zmienia, ale test przestaje działać, bo "JAK" uległo zmianie.

Nie możesz być zbyt agresywny wobec testów.

Anty-wzorce w testach: Powolne testy

Najważniejszą zaletą TDD jest szybkość sprawdzenia, czy dany kod i jego cel nadal działa. Nie musisz uruchamiać całego serwera. Uruchamiasz test i wiesz, czy wszystko działa tak jak dawniej.

Problem jednak się pojawia, gdy same testy zaczynają działać wolno. Cały ten cykl RED/GREEN/REFACTOR wydaje się wtedy strasznie głupi, bo równie dobrze mógłbyś uruchomić cały serwer i ręcznie sprawdzić, czy kod działa.

Nie chciałbyś czekać kilku minut, aby się dowiedzieć czy nowa zmiana w kodzie wykonuje test.

A jak w ogóle można napisać testy, które są wolne.

Powolne testy ostrzegają Cię.

  • Powolne testy mówią Ci, że twój kod jest zbyt powiązany ze sobą. Jeżeli do testowania danego scenariusza potrzebujesz ogromnej ilości przygotowanych obiektów, to chyba coś poszło nie tak w twojej architekturze.
  • Powolne testy też mogą Cię ostrzegać przed tym, że testujesz coś, co nie powinno być testowane. Kod może jest zbyt powiązany ze sobą i nic nie możesz z tym zrobić. Być może testujesz jakiś zwariowany przypadek: Co się stanie, gdy umieszczę 1.000.000 elementów do listy. Tak do tego są inne testy. Nazywamy je testami wydajnościowymi.

Im wolniejsze testy masz, tym gorzej będzie z twoją produktywnością w TDD. Z wolniejszymi testami będziesz miał także wolniejsze cykle wydawania aplikacji. 

Ograniczenia TDD: Możliwe dziury w testach

TDD nie jest srebrną kulą, która zabije każdy problem i uczni Ciebie nieomylnym programistą. 

Po pierwsze pamiętaj, że w testach możesz mieć dziury.  Pomyśl, dlaczego błędy na produkcji często istnieją, nawet w aplikacjach gdzie pracuje tysiące programistów i żywych testerów.

Co poszło nie tak? Na pewno te błędy przeszły przez testy

O ile TDD nam pomaga w znajdowaniu przyczyn błędnego działa kodu, jak i samych błędów. To jednak ja bym uważał z pewnością siebie co do tego, że po testach kod jest nietykalny

Błędy nadal będą istnieć. TDD ograniczy je, ale nie usunie problemu raz na zawsze, jakby to miało być definitywne starcie dobra ze złem.

Ograniczenia TDD: TDD w samo sobie nie jest wystarczające

TDD jest wspaniałą metodyką i daje nam genialne narzędzia. Na pewno lepiej jest spędzić trochę czasu na napisanie testu niż szukać potem 4 dni co nie działa w gigantycznej aplikacji.

Jednakże TDD nie jest wystarczające.

Aby mieć pewność, że wszystko jest w porządku, z twoją aplikacją musisz także np.

  • Sprawdzać proces wdrażania samej aplikacji na serwer lub na chmurę
    • Gdy wygrywasz nową wersję aplikacji, nigdy nie masz pewności, jak to będzie działać. Może stare pliki zostaną, a nowe się nie pojawią. Tego TDD nie sprawdzić i do tego masz inne narzędzia DEV-OPS jak Docker
  • Patrzyć na zmiany w sieci?
    • Znasz to? Aplikacja przestaje działać, ponieważ adresy usług WCF lub REST uległy zmianom. TDD tego nie sprawdzi 
  • Mieć testy integracyjne, czyli całościowe
    • Wiele problemów zobaczysz, dopiero gdy aplikacja zostanie uruchomiona od początku do końca. Testy jednostkowe np. wspaniale działają na bazach danych w RAM-ie. Tymczasem na produkcji jest prawdziwa baza SQL Server i coś nie działa.
    • Testy jednostkowe jak sama nazwa wskazuje nie są testami całościowymi

Ograniczenia TDD: Potrzeba wsparcia zarządu i innych programistów

Słuchaj, o zaletach testów można pisać i gadać. Możesz przeczytać książkę o TDD. Poczytać ten właśnie wpis i jest cudownie.

Jednakże, aby korzystać z TDD w pracy, musisz mieć wsparcie zarządu i innych programistów.

Twoi koledzy z pracy, którzy w ogóle nie znają TDD, mogą nie widzieć, jakie są wspaniałe zalet tej metodyki. Problem z TDD też jest taki, że te zalety widać później, gdy masz wieloletnią aplikację. Na początku TDD wygląda jak strata czasu i obniżenie produktywności działania

Nie łatwo jest uświadomić komuś, że ten krótkowały ból przyniesie coś lepszego.

Jeżeli nie przekonasz osoby w firmie bądź sama firma nie ma zwolenników TDD, to co tutaj dużo mówić nie będziesz tworzył oprogramowania tą metodyką.

Robienie TDD samemu skończy się tym, że inny programista będzie uzupełniał twoje testy byle szybciej, aby jego kod mógł znowu się skompilować.

Pytanie: Czy Agile jest wymagany do TDD?

TDD na pewno super klei się z Agile. Jednakże samo Agile nie jest wymagane, abyś mógł tworzyć oprogramowanie metodyką TDD. Nie musisz czytać o tym, czym jest Agile i jak to ma się to TDD. W sumie to jak znasz już TDD, to pewne, filozofię z Agile już znasz.

Z TDD chcesz:

  • Weryfikować nowe wymagania aplikacji (przykładowo klient o coś prosi)
  • Sprawdzać, czy stary kod nadal działa tak samo. Testy regresji
  • Obniżasz koszt wspierania egzystującej aplikacji jak najbardziej jest to możliwe

Teraz pytanie, czy możesz korzystać z TDD będą w procesie Waterfall

Oczywiście, że tak. Jednakże pewne zalety TDD będą widoczne, gdy będziesz pracował z Agile.

Pytanie: Czy naprawdę muszę pisać testy najpierw?

Oto kontrowersyjne pytanie, o które ludzie kłócą się najbardziej. Jak to jest? Czy naprawdę musisz pisać test najpierw, czy coś się stanie, jak tego nie zrobisz?

Pamiętaj, mówimy tutaj o głównej zasadzie: RED-GREEN-REFACTOR

Przypomnijmy sobie, co chcemy osiągnąć z TDD? 

  • Chcemy uniknąć przeinżeniorowania kodu, czyli robienia więcej niż potrzebujemy. Pisząc test najpierw, unikamy tego problemu.
  • Chcemy zwiększyć momentu i naszą pewność, że kod działa dobrze. Oczywiście możesz, to zrobić pisząc kod najpierw. Z testami jednak wyjdzie Ci to lepiej

Pamiętaj tutaj, nie chodzi o przestrzeganie zasad TDD, jakby to była święta księga przykazań. Twoim celem jest lepszy efekt.

Jeśli z jakiegoś powodu w którymś cyklu musisz najpierw napisać kod, a potem test to świat zapewne się nie zawali. Jednak jeśli zrobisz, to już wiele razy to możesz zadać sobie pytanie, po co w ogóle Ci te TDD.

Czym jest TDD?

Z każdym wymaganiem piszesz kolejny test. Mając test, piszesz potem kod, a potem go poprawiasz.

Z TDD skupiasz się na wymaganiach klienta i unikasz przeinżeniorowania (wiem, nie ma takiego słowa w języku polskim) swojego problemu.

Z TDD masz też większą pewność, że twój kod działa i tworząc kolejne cegiełki, szybko sprawdzasz, czy czegoś nie popsułeś, co daje momentum twojej pracy.

Z TDD możesz ulepszać cały czas kod i proces bez bólów głowy. Lepiej będziesz wspierał aplikację, która ma już 5-20 lat. TDD też jest twoją dokumentacją, a nawet dziennikiem twojej podróży po tworzeniu danej aplikacji.

Z TDD istnieje wiele pokrewnych tematów jak: 

  • Agile Development
  • Refactoring 
  • Continuous Integration
  • Continuous Deployment

Twoja podróż więc nie musi więc kończyć tutaj.

Jeżeli chodzi o ten cykl wpisów, to tak jak obiecałem, zostało mi jeszcze napisać krótkie porównanie frameworków UnitTest w .NET.