[UT-6.1] Jak nazywam testy

13

[ten post jest częścią mojego minicyklu o testach, pełna lista postów: tutaj]

Przeglądając kod wielu projektów, zarówno komercyjnych jak i open source, można spotkać całą masę konwencji nazewniczych stosowanych do klas i metod testujących. Dzisiaj przedstawię kilka moich zasad w tej materii wraz z uzasadnieniem. Wychodzę z założenia, że bardziej niż konwencja, standard czy “przyjęta dobra praktyka” liczy się czytelność pisanego kodu i łatwość powrotu do niego nawet po kilku miesiącach od napisania. Dlatego też w swoich projektach nie mam zdefiniowanej jedynego słusznego schematu nazywania testów. Jednym może się to podobać, a innym nie… ale z doświadczenia wiem, że najzwyczajniej w świecie takie podejście się sprawdza.

CamelCase – nie; pokreślenia – tak

Konwencja (notacja?) CamelCase jest bardzo fajna i przydatna. Podczas “normalnego” programowania jak najbardziej ją stosuję i jestem z niej zadowolony. Szczególnie gdy da się taki kod przetrawić Resharperowi i skorzystać z jego dobrodziejstw do nawigacji. Testy jednak nie są “normalnym” kodem, bo… mają znacznie dłuższe nazwy. CamelCase sprawdza się wyśmienicie dla nazw składających się z dwóch, maksymalnie trzech słów. Dłuższe ciągi są już trudne do przetrawienia dla “serca i umysłu”. Którą z tych linijek łatwiej przyswoić?

throws_if_user_enters_invalid_credentials_twice

ThrowsIfUserEntersInvalidCredentialsTwice

Ja bez wahania odpowiem, że zdecydowanie pierwszą. A o to przecież chodzi – żeby kod raz napisany dało się w miarę bezboleśnie wielokrotnie przeanalizować. Pierwotnie podchodziłem do tej koncepcji bardzo sceptycznie (a bo to przecież niezgodne z guidelines!), ale pragmatyzm wygrał, na szczęście, po raz kolejny.

Zdania twierdzące

Nazwa testu powinna w jednoznaczny sposób opisywać jaki dokładnie scenariusz jest testowany. I chodzi nie tylko o kontekst wykonania testu, czyli konfigurację środowiska, ale także o oczekiwany rezultat. Test o takiej nazwie:

what_if_user_enters_invalid_credentials_twice

niewiele mi mówi. Muszę zajrzeć w jego kod, aby dowiedzieć się “co się w takim razie ma wtedy stać?“. Zamiast tego o wiele bardziej wolałbym zobaczyć nazwę na przykład taką:

throws_error_if_user_enters_invalid_credentials_twice

Albo:

redirects_to_password_reminder_page_if_user_enters_invalid_credentials_twice

Nie boję się tworzyć nazw długich. Kluczowe jest, aby spełniały swoje najważniejsze zadania: opisywały scenariusz testowany w danym teście. Niejednokrotnie zdarza mi się pisać nazwy zajmujące sporo, sporo więcej niż jeden ekran. Dzięki temu, gdy przychodzi co do czego (czyt.: dostaję raport z testami nieprzechodzącymi, bądź czytam swoje testy sprzed X czasu aby zobaczyć dlaczego coś zrobiłem w sposób taki a nie inny), posiadam komplet kompilowanych, zsynchronizowanych z kodem informacji o danym kawałku systemu.

Przykład naprawdę długiej nazwy:

fetches_attributes_values_for_objects___prevents_loading_empty_objects_with_ID_value_only_caused_by_incorrectly_setting_selection_range

I kolejny:

removes_duplicate_nodes_based_on_object_and_source_definition_before_persisting___fixed_problem_with_nodes_being_saved_more_than_once_after_refreshing_node_client_side

Jak widać, w tych testach podałem nie tylko oczekiwane zachowanie, ale również jego UZASADNIENIE. Może to się wydawać nie do zaakceptowania, ale… naprawdę zdaje egzamin. Owszem, można zamiast tego dodać zwykły komentarz. Z komentarzami jest jednak pewien problem: dość szybko (na pewno szybciej niż nazwy metod) ulegają rozsynchronizowaniu w stosunku do komentowanego kodu. Dlatego też staram się ich unikać gdzie to tylko możliwe.

BDD-style?

Swego czasu eksperymentowałem z biblioteką MSpec. Bardzo spodobał mi się promowany przez nią sposób organizacji testów w klasach. Od tamtej pory liczba klas testowanych przestała się w moich projektach równać liczbie klas testujących. Klas testujących mam o wieeele więcej. Dość często tworzę jedną klasę testującą per jeden scenariusz mogący wystąpić w systemie. Wróćmy do przykładu z dwukrotnym wpisaniem błędnych danych do logowania:

redirects_to_password_reminder_page_if_user_enters_invalid_credentials_twice

Standardowo taki test znalazłby się zapewne w klasie nazwanej AccountControllerTests, czy coś w ten deseń (tak, wiem, że jest to sprzeczne z zasadami przedstawionymi w jednym z poprzednich postów, ale załóżmy że na dzisiejsze potrzeby napisaliśmy zły, fe, fat controller:)). Po przygodzie z MSpec zrealizowałbym to jednak (w xUnit) inaczej…

Prawdopodobnie na początek stworzyłbym abstrakcyjną klasę account_controller_test_base, która w konstruktorze tworzy kontroler i jego zależności, zapisując to wszystko jako pola protected. Następnie dodałbym klasę dziedziczącą o nazwie when_user_enters_invalid_credentials_twice – i w jej konstruktorze przygotował wszystko do poprawnej symulacji podania błędnych danych po dwakroć. W tej dopiero klasie znalazłyby się testy badające CO ma się stać w takiej sytuacji, więc właśnie tu pojawiłaby się metoda: redirects_to_password_reminder_page. Plus może sends_warning_email_to_administrator. Albo, jeśli miałbym bardziej przemyślaną architekturę: publishes_event_about_possible_hacking_attempt.

Dzięki takiej strukturze rozbijam swoje klasy z testami na mniejsze, bardziej skoncentrowane i zorganizowane byty. A raport z wykonania takich testów może być namiastką całkiem niezłego dokumentu prezentującego dokładne zachowanie systemu (a’la BDD, choć z takimi stwierdzeniami wolę być ostrożny).

Numerki – precz!

Najmniej przydatne nazwy testów to wg mnie te wygenerowane wg schematu [jakaśnazwa][numer]. Czyli na przykład:

RegexParser_Parse_Test1

RegexParser_Parse_Test2

RegexParser_Parse_Test3

Co nam to mówi? NIC! Z nazwy klasy wnioskuję, że ma ona za zadanie parsować wyrażenia regularne, ale… jak to robi? Keine idea. Nie mogę zerknąć na listę testów i dowiedzieć się czegokolwiek. Muszę analizować całą klasę z testami. Albo, co gorsza – implementację klasy testowanej.

Polemika?

Podejrzewam, że powyższe postulaty mogą wywołać zdecydowany sprzeciw. Mam kilka przykładowych argumentów niezgadzających się z takim podejściem do nazywania testów… mam też na nie krótkie odpowiedzi.

Takie nazwy są niezgodne z naszymi standardami. One są niezgodne z jakimikolwiek standardami!

Odpowiedź krótka: no to co? Standardy są po to, aby ułatwiać życie, a nie je utrudniać. Ogólne zasady, stworzone do zachowania spójności w “produkcyjnym” kodzie, nie mają tu moim zdaniem zastosowania. Główny powód jest bardzo prosty: testów nigdy nie wywołujemy w kodzie. Dla testów nie jest nam potrzebne intellisense – więc mogą być długie. Nie muszą zaczynać się z wielkiej litery – nie “zabrudzi” to nam API. Wiadomo, że zawsze metoda powinna w miarę dokładnie opisać zawarty w niej kod, ale dla testów tych informacji jest po prostu więcej niż w “zwykłym” kodzie.

Po co szczegółowe nazwy – od tego są komentarze

Tak jak pisałem wyżej – komentarze mają irytującą tendencję do opisywania kodu który BYŁ pod nimi w momencie ich pisania, a nie tego który znajduje się tam teraz aktualnie. Są także niestety niezwykle podatne na “nieumyślne” mnożenie się metodą kopiuj/wklej wraz z komentowanym kodem, który zaraz po “wklej” jest poddawany obróbce.

Jak test się zepsuje to i tak od razu trzeba pójść do kodu i go naprawić, a wtedy już będzie wiadomo co robi

Rola testów nie powinna ograniczać się do zapalania zielonych i czerwonych lampek. Testy mogą służyć jako doskonała dokumentacja testowanego kodu i odpowiednie ich nazwanie może w tym znacząco pomóc. Dobrze nazwane testy umożliwią zerknięcie na raport z ich wykonania i podstawie samego takiego raportu, bez wnikania w jakąkolwiek implementację, wywnioskowanie jakie jest zachowanie testowanej klasy czy nawet całego komponentu. Testów nie czyta się tylko wtedy, gdy “przestają działać”. Wręcz przeciwnie – one mogą być idealnym miejscem do rozpoczęcia zapoznawania się z kodem (czy to nowemu w projekcie programiście, czy też samemu autorowi za kilka miesięcy).

Ale VS generuje mi takie nazwy testów!

To jest argument tak bezsensowny, że sam bym na niego nie wpadł… Nie wiedziałem nawet co i jak generuje VS, ale już wiem:). Na taki tekst odpowiedzieć można chyba tylko poradą zabarwioną nutką złośliwej ironii: to napisz sobie makro generujące losowy string i stosuj do wszystkich metod, nie tylko do testów, będzie szybciej… no i VS ci tak wygeneruje!


Jaką macie opinię na ten temat? Jak nazywacie swoje testy, co byście dodali do powyższej listy? Lub co byście z niej usunęli i dlaczego? Jak zwykle czekam na komentarze.

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.

13 Comments

  1. podpisuję się rękami i nogami :) zaczynałem od CamelCase-a ale jednak ciężko się czyta długie nazwy więc zacząłem stosować WhenCostamSieStanie_ThenCosPowinnoSieStac ale to też nie działało. Alt+Enter(ładnie zamieniał na podkreślenia) i wszystko z podkreśleniami i wszystko jasne i czytelne :)

    Co ważne, po zmianie logiki testu warto popatrzeć na nazwę testu i upewnić się że jest jeszcze aktualna. Taką sytuację miałem kilka razy przy tzw przeoraniu tzn dostosowaniu całego systemu do nowych założeń. W codzienniej pracy to jest raczej incydentalne – co nie zmienia faktu że warto na to zwracać uwagę.

  2. @rek,
    A no oczywista sprawa z tym pilnowaniem aktualności nazw:). One też mogą "się rozjechać", ale moim zdaniem rzadziej niż komentarze. Jednak na kompilowany kod zwraca się większą uwagę.

  3. W firmie dla której teraz pracuje stosujemy dokladnie taka konwencje, MSpec i podkreslenia. Bardzo przyjemnie sie to pozniej czyta i analizuje. Moze jednym z minusow jakie widze w MSpec to to ze czasami sie sporo trzeba naklepac, implementacje When_xxxx, It_xxxx, Because_, Establish_… ale z drugiej strony jest to zdecydowanie warte zachodu :-) Mozna sobie pomoc dziedziczeniem z klas abstrakcyjnych oraz Behave jednak i tak tone tego tekstu sie produkuje.

  4. Dawid Kowalski,
    MSpec jest chyba najfajniejszą biblioteką do testów z jaką miałem przyjemność pracować. Niestety minus ma taki, że dla osób nie-do-końca-programistycznych "próg wejścia" jest dość wysoki – te wszystkie delegaty, lambdy itd. Ze standardowymi bibliotekami, jak chociażby xunit, łatwiej jest krzewić piękno unit testów tam, gdzie jeszcze zakrzewione nie jest:). Niestety o ile przeniesienie idei "Establish" i "It" jest proste, to dość ciężko jest klarownie zasymulować "Because".

    I w sumie nie pamiętam żebym jakoś szczególnie musiał więcej klepać niż teraz, używając tylko xunit. Odpowiednie ‘live templates’ w R# załatwiały sprawę.

  5. Procent:
    Bylem na Code Retreat z wlasnym notebookiem i "krzewilem" MSpec wsrod ludu Javowego, jak im kazalem nie patrzec na te wszystkie = () => to nawet szybko podlapywali :-)
    Z tym klepaniem mam na mysli ze wczesniej moje opisy testow byly hmm, mniej opisowe :-) MSpec niejako zmusil mnie do poprawnego pisania co i w jakim kontekscie chce i oczekuje. Znacznie lepiej to wyglada.

  6. Dawid,
    No i gites. W sumie "przedstawienie zalet MSpec" to fajny pomysł na posta, dopisałem sobie do listy "na kiedyś":)

  7. A ja polecam The Art Of Unit testing (Roy Osherove). W ksiazce jest pelno bezcennej wiedzy, miedzy innymi sprawdzona konwencja nazewnictwa testow itp. Mi bardzo przypadla do gustu i stosuje z powodzeniem juz od dluzszego czasu (TestowanaMetoda_Warunki_Wynik). Dodatkowo cialo testu jest ulozone wg. zasady AAA – Arrange, Act, Assert. Trzymajac sie tych 2 zasad testy sa naprawde bardzo czytelne :)

  8. Ja używam podkreśleń i zdań twierdzących, czasem tylko zaczynam je od "It", jak np. "It_should_process_a_valid_request". Do każdego pliku dodaję też instrukcje, aby R# nie czepiał mi się o te nazwy. W środku metody stosuję albo AAA, albo mały wewnętrzy DSL, np.

    public void Validation_should_fail_when_there_is_no_Customer_element()
    {
    const thisXml = @"…"
    Validating(thisXml).ShouldFailBecause("No Customer element");
    }

  9. maciek,
    Książki nie czytałem, ale takie nazewnictwo próbowałem – nie zdaje u mnie egzaminu. Ale AAA jak najbardziej.

  10. Szymon,
    Stosowałem konwencję "it_should_…" jak używałem MSpec, ale doszedłem do wniosku że to zbędne literki. "it_should_redirect_to_page_X" znaczy to samo co "redirects_to_page_X", a jest mimo wszystko trochę dłuższe.

  11. Odpowiedź Ayende to czysto akademicki spór. Tak naprawdę chodzi o konwencję, której się użyje a która wniesie pewien porządek. Stosowanie konwencji zwykle nie wiąże się z dodatkowym nakładem pracy tylko ze zmianą przyzwyczajeń.
    Więc co kto lubi. mnie akurat podejście Phila pasuje, zmotywowało mnie natychmiast do pisania testów w moim nowym systemie :)

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ą!