Niezawodny sposób na naukę testów jednostkowych

26

Przez ostatnie dwa lata pojeździłem sporo po firmach ze swoim (zajebistym zresztą ;) ) szkoleniem o testach jednostkowych. Dzięki temu sam uświadomiłem sobie, w jaki sposób posiadłem “wiedzę tajemną” na temat testowania.

Wszystko stało się prostsze, gdy odkryłem, że:

Pisanie testów do istniejącego kodu jest jak gra wstępna po seksie

Testy mają bardzo wiele zalet i służą więcej niż jednemu celowi. Wykorzystanie w pełni ich potencjału jest możliwe tylko wówczas, gdy napiszemy je PRZED testowanym kodem. O różnych rolach testów rozpiszę się innym razem.

TDD or not TDD?

Tak popularne – i często źle rozumiane – Test-Driven Development można podzielić na dwa podejścia.

Podejście pierwsze to takie “prawdziwe” TDD, jak promuje je Uncle Bob Martin czy Kent Beck. Polega na tym, że piszemy testy przez chwilę. Tylko krótką chwilę: do momentu, gdy kod przestaje nam się kompilować. Albo gdy test “nie przechodzi”. Wtedy przeskakujemy do kodu “produkcyjnego” i klepiemy… przez kolejną chwilę. Do momentu, gdy kod zaczyna się kompilować i wszystkie testy przechodzą. Takie przeskakiwanie między kontekstami wymaga bardzo dużo dyscypliny i – szczególnie początkującym – na pewno sprawi sporo trudności. Pełny cykl “test -> kod” zajmuje często mniej niż minutę.

Podejście drugie, które rekomenduję, to “Test-First Approach“. Sprowadza się do prostej zasady: piszemy testy do kodu, którego jeszcze nie mamy. Tylko tyle i aż tyle. Często tworzę wiele testów zanim zabiorę się za kod, który sprawi, że będą one przechodzić. Ba, czasami nawet skupiam się na pisaniu PUSTYCH testów. Ich nazwy pozwolą mi zweryfikować, czy na daną chwilę spisałem w “kompilowalnej” postaci wszystkie wychwycone wymagania, scenariusze, wartości brzegowe. Upewniam się, że niczego nie pominąłem. Dopiero mając taką baterię czerwonych testów zabieram się do faktycznej implementacji wymagań.

Po co takie zabiegi? To proste:

 Kod trudny do przetestowania jest kodem trudnym do utrzymania.

A pisząc testy przed napisaniem właściwego kodu – tworzymy wyłącznie kod testowalny.

Więcej mówiłem na ten temat niedawno w podkaście DevReview (“DevReview #8 O testach z Maciejem Aniserowiczem“). Zachęcam do posłuchania, bo fajna rozmowa nam wyszła.

I pojawia się odwieczne pytanie:

Jak ZMUSIĆ się do pisania testów przed napisaniem kodu?

Wiem, jak ciężko jest zmienić swój sposób kodowania i przestawić się na podejście test-first. W końcu przyzwyczajenie, pielęgnowane od lat, nie umrze tak łatwo. Napiszę linijkę czy dwie, wcisnę F5, kliknę w aplikacji w odpowiedni guzik i tym samym WIEM, ŻE DZIAŁA. Po co mi tutaj testy? Ponownie: do tematu “po co mi testy” wrócimy kiedy indziej.

Zachęcam do przeprowadzenia prostego eksperymentu. U mnie zaproponowana zmiana wpłynęła na wszystko – całe moje podejście do programowania. Na szczęście sposób ten odkryłem stosunkowo wcześnie na swojej ścieżce. Aż dziw, że jeszcze (chyba) o tym nie pisałem.

Eksperyment polega na jednym prostym kroku:

 Pisz kod, którego nie da się uruchomić

W świecie .NET oznacza to: twórz tylko DLLki. Nie pisz kodu w projektach “aplikacyjnych”. Dla programisty chcącego nauczyć się testowania: pojęcia takie jak Main(), exe czy webapp nie istnieją. Zwykłe biblioteki są miejscem, w którym rośnie kodzik z logiką.

Faktyczna APLIKACJA to tylko sposób na zaprezentowanie kodu użytkownikom.

W momencie, w którym uwolnisz się od exeków, odkryjesz śmieszną prawdę: nie jesteś Linusem Torvaldsem. Nie jesteś w stanie zaufać sobie na tyle, aby taki kod pchnąć do repozytorium, dostarczyć do finalnego rozwiązania. Nie możesz go uruchomić, więc jak sprawdzić, że działa? Ano właśnie: pozostają Ci TYLKO TESTY.

Skoro Twój kod może być wykonany tylko i wyłącznie w kontekście testów jednostkowych, to zaczniesz przykładać do nich wagę. Kolejne linijki będą MUSIAŁY być napisane tak, aby testy w łatwy sposób się do nich dobrały. Wtedy przekonasz się też na własnej skórze, że życie jest o wiele prostsze, jeśli testy piszesz najpierw.

I to będzie pierwszy krok. Klęknij, devie. Bach, bach, mieczem bo barach. Powstań, sir-test-devie.

Wkraczasz w piękniejszy świat.

EDIT 06.10.2016: dodałem tekst na temat poruszony w komentarzach: “3 sprawdzone sposoby na wprowadzanie testów do istniejącego kodu“.

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.

26 Comments

  1. Kamil Ławniczak on

    Niby ta mini petla TDD ma zapewnic zawsze dzialajaca logike.,
    ale mysle ze w druga strone przesadzic tez nie jest dobrze, test mylacy gorszy niz brak.
    Test-Pass chyba najwygodniejsze w moim przypadku.

    AHA i pisanie testow tam gdzie ich nigdy nie bylo… sadomaso ;0

    • Kamil,
      No tak, we wszystkim, jak zawsze, potrzebny jest umiar :).
      A dopisywanie testów do już istniejącego/działającego systemu to… zupełnie odrębny temat.

  2. A co w przypadku istniejacego już kodu i potrzeby jego refaktoryzacji? Czy pisanie testów do takiego kodu to gra wstępna po seksie czy też niezbędna antykoncepcja?

    • Koper,
      Pisanie “na hurra” nie ma sensu, trzeba to robić z głową. To nic przyjemnego i często nie ma sensu. A o antykoncepcji ciężko tu mówić skoro fakt napisania kodu został już dokonany :).

  3. Siemka
    Myślałeś o tym, żeby przygotować szkolenia online dla freelancerów na solidnym poziomie?

  4. Było by to piękne gdyby nie JS.
    Znacie jakieś ciekawe narzędzia do pisania testów jednostkowych dla JS i do testów FrontEndu webowego?
    Ja znam Selenium które klika za mnie.

    • Paweł,
      Jasmine chociażby. Zasada jest podobna: testuj swój kod, a nie kod frameworka który aktualnie używasz. Pisz swoją logikę w separacji od angularów czy reactów – i testu jako osobny komponent.

    • Poza Jasmine jeszcze dosyć popularny zestaw to mocha+chai+sinon. Mocha to dość minimalistyczny framework do testów, chai to bardzo wygodna w użyciu biblioteka asercji a sinon to biblioteka do mocków.
      No i jak mowa o frontendzie, to na dłuższą metę nie obejdzie się bez Karmy – narzędzie, które uruchamia przeglądarki i wewnątrz nich uruchamia testy. Szczególnie przydatne do np. uruchomienia testów na raz na kilku przeglądarkach czy wpięcia testów w CI (Jenkins, Travis i reszta)

  5. Imho jednak “prawdziwe” TDD jest lepsze, a to ze względu na to, że:
    * Wystarczy analityczny tok myślowy, jaki zwykle mamy, kiedy wymyślamy rozwiązanie, od razu przelewać na testy
    * Kod produkcyjny wtedy robi dokładnie to i tylko to, co jest wymagane
    * Testy są jednocześnie dokumentacją działania kodu, nie zaś dokumentacją wymagań
    * Nie marnujemy czasu na zastanawianie się nad edge case’ami, które nigdy nie wystąpią

  6. W TDD można pisać gdy znamy technologię i mamy dobrze opisany cel biznesowy, gdy znamy wejście i wyjście programu. Bo jak nie wiemy, co chcemy osiągnąć, to nie napiszemy sensownych testów. Gdy robimy coś na czuja, w technologii, której się dopiero uczymy, albo coś dopiero badamy (bo np. nie wiemy, czy w ogóle potrzebne nam informacje możemy wyciągnąć) – tam podejście TDD znacznie spowolniłoby (czy wręcz uniemożliwiło) napisanie działającego kodu.

    Kolejną pułapką związaną z TDD jest fakt, że stosowanie go wszędzie jest stratą czasu. Są ludzie, którzy próbują w ten sposób testować jednostkowo bazę danych albo system plików, w efekcie tworząc coś, co jest kompletnie bezużyteczne z praktycznego punktu widzenia.

    “Kod trudny do przetestowania jest kodem trudnym do utrzymania.” – truizm. Ale z drugiej strony, przecież możemy sobie do kodu biznesowego dopisać metody, które łatwo uruchomimy w testach, możemy nawet metody biznesowe uczynić wirtualnymi, aby nadpisać je w klasach, które umieścimy w projekcie testowym i takimi koszmarnymi partial-mockami osiągnąć założone pokrycie. Kod biznesowy dalej będzie nietestowalny, ale dzięki naszym hakom coś zostanie przetestowane – najczęściej framework mockujący i kod dopisany specjalnie na potrzeby testów.
    Nie wymyśliłem sobie tego – takie rzeczy mam w obecnym projekcie.

    Co do pisania “bez interfejsu” – od zawsze mówię, że testy to powinno być pierwszy UI, który napiszemy do naszej aplikacji. Niestety, od dawna programowanie wygląda tak, że zaczyna się od GUI – bo przecież najważniejszy jest ładny wygląd i szybki efekt, a więc wszelką logikę wpycha się do kodu warstwy prezentacji. Tak samo było za czasów WinFormsów i WebFormsów, tak samo w czasach popularyzacji ASP.NET MVC, tak samo jest teraz w tych nowomodnych frameworkach JS.

    • “Co do pisania “bez interfejsu” – od zawsze mówię, że testy to powinno być pierwszy UI, który napiszemy do naszej aplikacji. ”

      To mi się podoba, ale to wymaga działania, o czym pisze Maciej, totalnego oddzielenia całej logiki od UI. Czasem, np. przy szybkim prototypowaniu, jest to trudne mentalnie :)

      • Moim zdaniem, to “oddzielenie” to jest właśnie coś, co odróżnia programistów od klepaczy.

  7. Czyli dlatego tyle nowych freamworków w javascriptcie powstaje. ;)

  8. Pingback: 3 sposoby na testowanie istniejącego kodu | devstyle.pl

  9. Pingback: Czy testy jednostkowe można traktować jako interfejs? Jak najbardziej! | devstyle.pl | Maciej Aniserowicz