[ten post jest częścią mojego minicyklu o testach, pełna lista postów: tutaj]
Na tak postawione pytanie aż chciałoby się odpowiedzieć: “testować wszystko, you fool!“. Życie uczy jednak, że takie podejście jest bardzo niepraktyczne i na dłuższą metę nie ma sensu. Dążenie do pokrycia 100% kodu mija się z celem i jest po prostu stratą czasu.
O powodach pisania testów pisałem na początku tego cyklu. Są jednak miejsca, w których koszt napisania testu jest bardzo duży, a jego wartość – znikoma.
Zacznijmy więc od odpowiedzi na trudniejsze pytanie.
Czego nie testować?
Unikałbym testowania wszelkiej maści wygenerowanego kodu. Nie mówię tylko o plikach *.designer.cs – to już ekstremum i chyba nikt się na to nie porywa. Pod pojęciem “wygenerowany kod” rozumiem także proste gettery/settery czy konstruktory. Takich konstrukcji nie powinno się pisać ręcznie – straszna strata czasu. A tym bardziej ich testować – strata jeszcze większa. “Biznesowy” zysk z potencjalnych testów tak banalnego kodu jest praktycznie zerowy. Z pomocą przychodzą narzędzia tak proste jak snippety w Visual Studio czy bardziej skomplikowane, jak szablony z Resharpera. Z nimi wprost nie sposób popełnić durny błąd wykrywalny przez proste testy… które swoją drogą mogą taki durny błąd powielić.
Wyjątkiem od “nietestowania wygenerowanego kodu” byłby oczywiście kod generowany przez nas, ale wtedy podpada to pod testy generatora a nie kodu wynikowego.
Żmudne, nudne, męczące i baaardzo kosztowne w utrzymaniu jest pisanie testów do klas mających wiele zależności i zajmujących się głównie przekazywaniem obiektów między nimi. Z jednej strony – powinniśmy po pierwsze unikać TWORZENIA takich klas… ale z drugiej strony – wiadomo jak to w życiu jest. Zwykle miejsca takie pełnią rolę koordynatorów przepływu logiki w naszej aplikacji, same w sobie logiki takowej nie zawierając. Testy sprowadzają się wtedy właściwie do stworzenia kilku(nastu?) mocków, podania ich jako zależności, a następnie skonfigurowania metod na tych mockach. Metoda na jednym mocku zwraca jakiś obiekt, a X testów sprawdza, czy wartość ta została przekazana drugiej metody, w innym mocku. I tak w kółko. Prowadzi to do takiej sytuacji, że klasa zawierająca testy będzie miejscem bardzo często odwiedzanym podczas refaktoringu kodu, dodania nowej zależności, przedstawienia dodatkowego interfejsu czy zmiany przepływu obiektów, nawet niemającego wpływu na “core” naszej logiki biznesowej. Kiedyś stworzyłem kilkaset podobnych testów, a potem utrzymywałem je przez prawie dwa lata. Wniosek mam prosty: testy często się “psuły”, a mimo to nie pomogły w wykryciu ani jednego (sic!) błędu. Trzeba było je aktualizować, a design aplikacji w żaden sposób z tego nie skorzystał. Mało tego – ich czytanie wcale nie mówiło więcej, niż po prostu zajrzenie w kod testowanych mechanizmów – ba, było nawet bardziej męczące.
O moim konkretnym scenariuszu rozpisywać się tutaj nie będę, ale przychodzi mi do głowy całkiem chyba niezła analogia. Wszyscy znamy i lubimy wzorzec MVC, ale czyha w nim jedna bardzo niebezpieczna, i często ignorowana (przyznaję: także przeze mnie) pułapka: pakowanie logiki biznesowej do kontrolerów. Na samym początku może wydawać się to normalną praktyką: kontroler przyjmuje dane, ewentualnie poddaje jakiejś bardziej skomplikowanej walidacji, grzebie sobie w bazie, wykonuje obliczenia, wywołuje metody, znowu grzebie w bazie, a na koniec generuje widok. Prędzej czy później (niestety raczej później niż prędzej, co czyni ją bardziej niebezpieczną niż może się wydawać) takie coś wróci do nas w postaci spaghetti-code i mega-wielkich klas przepakowanych wszelkimi odpowiedzialnościami, mającymi ze sobą wspólnego tylko tyle, że użytkownik widzi je pod podobnym adresem URL. Kontroler powinien, jak sama nazwa wskazuje, kontrolować/nadzorować wykonanie logiki biznesowej, ale nie mieć o niej wielkiego pojęcia. Od logiki jest… tak, Model! W przeciwnym wypadku stosujemy anti-pattern zwany “fat controller”.
I po tym przydługim wywodzie stwierdzenie, które może być uznane za dość kontrowersyjne: poprawnie napisane kontrolery są właśnie koordynatorami nienadającymi się do testowania. Zawierają minimalną ilość banalnego kodu. Konieczność napisania testu dla kontrolera traktuję jako znak ostrzegawczy, że coś jest z kontrolerem nie tak – robi więcej niż proste przekazanie sterowania do modelu/domeny i wygenerowanie widoku.
Bajdełej, podobnie rzecz się ma z klasami “code-behind” znanymi z WebForms (albo analogicznymi rozwiązaniami, jak chociażby kodem “pod designerem” w Workflow Foundation). Można je przetestować, ale ich zawartość powinna być maksymalnie uproszczona i momentalnie przekazująca sterowanie “gdzieś dalej” (w przypadku WebForms na przykład do presenterów z wzorca MVP). Czasami co prawda sam framework wymusza na nas konieczność wydziergania niemałej liczby linii kodu w celu pokazania/ukrycia odpowiednich elementów czy zbindowania danych, ale programista na pewno znajdzie bardziej pożyteczne zajęcie niż pisanie do tego testów.
A skoro już wspomniałem o widokach…W internecie niejednokrotnie natknąłem się na próby testowania kodu zawartego w plikach *.cshtml. Pewnie w jakiś magiczny sposób można by robić to samo z *.aspx/*.ascx. Moim skromnym zdaniem mija się to z celem. Rola kodu widoku jest prosta: pokazać dane. I nawet jeśli mamy tam jakiegoś ifa to nie jest rolą testów jednostkowych sprawdzanie działania tych instrukcji. Takie coś można badać testami symulującymi przeglądarkę, ale powody inwestowania w nie czasu są trochę inne. To jednak temat na osobną notkę.
Nie powinniśmy się też skupiać na szczegółach implementacji testowanej logiki. Mamy zbadać i udowodnić “co”, a nie “jak”. Dostarczamy input, odbieramy output i tworzymy asercje sprawdzające czy wynik zgodny jest z oczekiwaniami. Dlatego też jestem przeciwny stosowaniem tzw. “strict mock” – czyli kontrolowaniu na poziomie testu kolejności wywołania metod na jakiejś zależności, badaniu ile razy dana metoda została wywołana czy upewnianiu się, że wywołano tylko metody przez nas przewidziane, a nie jeszcze jakieś inne.
Co testować?
Mówiąc prosto: wszystko pozostałe. Jakakolwiek logika biznesowa powinna mieć dokumentację w postaci testów. Idealnie, jeśli testy powstają przed implementacją owej logiki, ale nie o TDD jest ten post.
Staram się jednak podchodzić do tego zdroworozsądkowo – jeżeli pisany kod jest prawdziwie banalny to raczej nie zainwestuję czasu w pokrycie go testami. Jeśli jednak nawet w krótkiej metodzie pojawia się chociażby IF – to sytuacja ulega zmianie i wtedy kilka testów staram się rzucić. Nawet nie po to, żeby sprawdzić poprawność kodu. Raczej po to, żeby zostawić na przyszłość ślad mówiący “tak, to działanie jest zamierzone, świadomie zaimplementowałem to w taki a nie inny sposób, a testy o odpowiednich nazwach są tego dowodem“.
Testuję też wyrzucanie wyjątków przez moje metody – i testy uwypuklają scenariusze, w których taka sytuacja zachodzi. Każda świadoma instrukcja “throw” znajduje odzwierciedlenie w teście – oznacza to, że takie zachowanie jest poprawne i pożądane.
Idealnym obiektem testów jednostkowych są wszelakie algorytmy – kawałki kodu robiące coś z dostarczonymi danymi i zwracające wynik operacji na nich, bez udziału zewnętrznych zależności. To chyba wymarzone środowisko dla unit testów.
Warto również pamiętać, żeby w testach przewidzieć nie tylko sytuacje badające “czy kod robi co powinien”, ale także “czy kod nie robi tego czego nie powinien”. Pierwszy z brzegu przykład: pisząc test dla metody zwracającej artykuły opublikowane przez danego użytkownika przydałoby się również napisać drugi test sprawdzający, czy przypadkiem dodatkowo metoda nie zwraca artykułów opublikowanych przez innych użytkowników. Albo jeśli jakaś reguła biznesowa ma dać rabat uprzywilejowanemu klientowi to miło byłoby widzieć w testach weryfikację, że “normalny” klient przekazany do danej reguły owego rabatu nie otrzyma.
Nie ukrywam, że zdecydowanie ważniejszą częścią niniejszego posta są akapity traktujące o nietestowaniu. Z mojego doświadczenia wynika, że mądre inwestowanie czasu spędzonego na pisaniu i utrzymywaniu testów jest jedną z najtrudniejszych decyzji podejmowanych podczas tworzenia systemu. Posiadanie masy zbędnych, niespełniających swej roli testów może być bardziej kosztowne i spowalniające niż ich całkowity brak.
Jakie macie przemyślenia na ten temat? Czy dążenie do pokrycia 100% kodu testami to Waszym zdaniem dobry pomysł? Jakie testy można wyeliminować, a jakie są ważne? Może ktoś nie zgadza się z tym co napisałem? Chętnie poznam alternatywne opinie (poparte oczywiście stosownymi argumentami:) ).