SlideShare a Scribd company logo
1 of 8
Download to read offline
Wiele zostało już napisane na temat anty-wzorców jakie obserwujemy w świecie testów automatycznych. James Carr i jego „TDD Anti-Patterns” to lektura obowiązkową dla
każdego, kto chce uniknąć podstawowych błędów. W tym artykule chciałem się skupić na błędach mniej oczywistych, decyzjach, które w momencie podejmowania wydają się
rozsądne lub też zupełnie niegroźne, a które prowadzą do nieoczekiwanych rezultatów. Całość ujęta jest w ramy siedmiu grzechów głównych choć jak się przekonacie błędów
(podobnie jak grzechów) jest znacznie więcej.
1. Obżarstwo
Już papież Grzegorz I wyszczególnił 5 dróg, które wiodą do popełnienia grzechu obżarstwa... Ja podążę tym samym tropem.
a) Praepropare – testowanie zbyt wczesne. Wyobraźmy sobie system, którego rolą jest wysyłanie raportów w oparciu o wiadomości wejściowe. Raport zawiera w sobie identyfikator
wiadomości wejściowej, która jest jego katalizatorem. Operacja wysyłania raportów jest oczywiście asynchroniczna względem wykonania testu, toteż musimy poczekać na
odpowiedź. Test możemy skonstruować zatem na dwa sposoby:
@Test
public void shouldGenerateReportCorrespondingToInboundMessage() {
InboundMessage message = new InboundMessage(id, legalEntity);
sender.send(message);
await().atMost(TEN_SECONDS).until(appropriateReportIsReceived(message));
}
private Runnable appropriateReportIsReceived(InboundMessage message) {
return () -> reportListener.listenFor(aReport()
.thatHasId(message.getId())
.thatIsAgainstLegalEntity(message.getLegalEntity())
);
}
@Test
public void shouldGenerateReportCorrespondingToInboundMessage_2() {
InboundMessage message = new InboundMessage(id, legalEntity);
sender.send(message);
await().atMost(TEN_SECONDS).until(correspondingReportIsReceived(message));
Report report = reportListener.getReport(id);
assertThat(report, is(aReport().thatIsAgainstLegalEntity(legalEntity)));
}
private Runnable correspondingReportIsReceived(InboundMessage message) {
return () -> reportListener.listenFor(aReport().thatHasId(message.getId()));}
Pierwsze podejście jest nieco bardziej czytelne aniżeli podejście drugie, zatem zadajmy sobie pytanie czy jest powód dla którego poświęcilibyśmy czytelność testu. Jednym z
głównych powodów dla których inwestujemy w automatyzację testów jest szybkość z jaką uzyskujemy informację zwrotną. Rozważmy raport wygenerowany dla danej wiadomości
wejściowej w sytuacji gdy posiada on złe legalEntity:
I) w podejściu pierwszym o tym fakcie dowiemy się dopiero po upływie zadeklarowanego czasu oczekiwania (w naszym przypadku 10s). Dodatkowo informacja o błędzie
będzie bardzo ogólna, dowiemy się jedynie iż raport o zadanym id oraz legalEntity nie pojawił się w oczekiwanym czasie – zwróćmy uwagę na to, iż jest to informacja nie
tylko zbyt ogólna ale co gorsze wprowadzająca w błąd naszą intuicję.
II) W podejściu drugim o błędzie dowiemy się zaraz po otrzymaniu raportu i będzie on bardzo dokładnie zidentyfikowany
Dodatkowo czytelność drugiego rozwiązania możemy nieco poprawić enkapsulując oczekiwanie na raport w klasie ReportListener.
b) Laute – testowanie zbyt drogie. Ogromna bolączka zwinnych testów, choroba którą zwykliśmy diagnozować dopiero, gdy jest już w bardzo zaawansowanym stadium. Dobrą
regułą, która pozwoli nam szybko poczuć iż choroba się właśnie zaczęła, lecz również pozwoli nam wyciągnąć maksimum korzyści z inwestycji w testy automatyczne, jest
uruchamianie kompletnego zbioru testów po każdej zmianie kodu źródłowego. Musimy sobie zdawać sprawę z tego iż często optymalizacja szybkości wykonania testu może
pociągać za sobą zmianę kodu produkcyjnego.
I) Pierwszą, najważniejszą regułą optymalizacji kosztów testowania to umiejscowienie testu na odpowiednim poziomie automatycznej piramidy testów, przy czym złota reguła
piramidy mówi: im niżej tym taniej. Oczywistością jest, że nie każdą funkcjonalność będziemy w stanie przetestować na poziomie testów jednostkowych.
II) Wyobraźmy sobie iż nasza aplikacja wykonuje jakąś czynność periodycznie. Zaimplementowaliśmy scheduler który odpala się zgodnie z cron'owym harmonogramem. Aby
sensownie integracyjnie przetestować taką funkcjonalność możemy zbudować część kontekstu dookoła klasy wyzwalanej przez scheduler (bez samej funkcjonalności
automatycznego odpalania) i uruchomić jej odpowiednią metodę publiczną. Co możemy jednak zrobić gdy chcemy przetestować integrację tej funkcjonalności z
pozostałymi modułami naszego systemu? Zmienić kod produkcyjny w dwojaki sposób:
1. dodać interfejs (jmx, rest, rmi, etc.) pozwalający na uruchomienie owej funkcjonalności w trybie „na żądanie”
2. dodać możliwość wyłączenia scheduler'ów.
III) Wyobraźmy sobie iż nasz system wysyła raporty dla określonych wiadomości wejściowych, acz są też takie wiadomości dla których nasz system nie wysyła żadnych
raportów... jak automatycznie przetestować fakt, iż raport nie został wysłany? Najprostszy, brutalny sposób to oczywiście zaimplementować timeout w naszym teście, przez
co domyślnie stwierdzamy „jeśli nie zdarzyło się do tej pory to znaczy, że się już nie zdarzy”. Taki test szkodzi całemu zbiorowi testów w dwojaki sposób:
1. znacząco wydłuża czas trwania testów (za każdym razem musimy czekać cały timeout)
2. potencjalnie wprowadza „migotanie testów” (test czasem przejdzie, czasem nie). Wyobraźmy sobie prostą sytuację gdy nasz serwer ciągłej integracji jest bardzo
obciążony i nagle nasz system zaczyna zwalniać, przez co dla zadanej wiadomości wejściowej system wyśle błędnie raport ale zazwyczaj dopiero po timeout'cie i tylko
czasem uda mu się wysłać go szybciej. W ten sposób ludzie przyzwyczają się, że jest jeden test który „migocze” więc.... trzeba go puścić jeszcze raz bo przecież
zmiany z czerwonym testem nikt nam nie pozwoli włączyć do master'a/trunk'a.
Odpowiedzią na przedstawiony problem jest kolejna złota reguła testowania automatycznego: nie można udowodnić/przetestować że coś się nie stało (musielibyśmy
czekać nieskończenie długo.... a to trochę za długo). Jedynym sensownym sposobem na wybrnięcie z tego impasu jest przetestować, że coś się stało ;-). To może wymagać
sporych zmian w samym systemie (celem jest poprawienie testowalności). Jakkolwiek skomplikowane technicznie by to nie było, koncepcyjnie sprowadza się do bardzo
prostej rzeczy, test musi wiedzieć iż procesowanie danej wiadomości się skończyło i jest jakiś marker na, który musi czekać.
c) Nimis /Ardenter – testowanie w zbyt dużej ilości , zbyt dokładne. Intuicyjnie może się nam wydawać, że im więcej przetestujemy tym lepiej... nic bardziej mylnego. Zrobimy sobie
potencjalnie krzywdę na trzy sposoby:
I) tracimy czytelność kontraktu jaki nasza klasa zawiera ze swoimi kolaboratorami
II) tracimy czas na utrzymywanie większej ilości kodu
III) wydłużamy sobie czas trwania naszego zbioru testów. Jeśli dopuścimy się takiej nierozwagi na poziomie jednostkowym to owe wydłużenie będzie zazwyczaj marginalne,
lecz wystarczy wejść po stopniach piramidy testów nieco wyżej by czas oczekiwania na testy znacząco się wydłużył.
Co to zatem znaczy „zbyt dużo” i jak się przed tym uchronić? Stare, dobre praktyki wypracowane w czasach intensywnych testów manualnych przychodzą nam z pomocą: klasy
równoważności, wartości brzegowe, pokrycie diagramu przepływów, tabele decyzyjne. Opis tych metod wykracza poza ramy tego artykułu ale też jest dużo dokumentacji na ten
temat w sieci. Chciałbym się skupić na czymś czego w sieci jeszcze nie znalazłem otóż na sposobie poradzenia sobie z testami automatycznymi skomplikowanych warunków
logicznych. Z jednej strony nic prostszego jak użyć testów parametrycznych i przetestować cały zbiór kombinacji... z drugiej strony będąc tak dokładni całkowicie zaciemniamy
obraz odpowiedzialności naszej klasy. Musimy znaleźć zatem jakiś sensowny kompromis. Rozważmy następującą w sumie dość prostą implementację która bazuje decyzję na
podstawie 4 parametrów:
public boolean isEligible(InboundMessage message) {
LegalEntity legalEntity = message.getLegalEntity();
Counterparty counterparty = message.getCounterparty();
PartiesRelation partiesRelation = relationService.determineRelation(legalEntity, counterparty);
Product product = message.getProduct();
return legalEntity.isEligible() &&
counterparty.isEligible() &&
partiesRelationship.isEligible() &&
product.isEligible(
}
By dokładnie przetestować powyższy kod musimy pokryć 16 (24
) przypadków:
public Object[] eligibleParameters() {
return $(
$(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship, true),
$(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship, false),
$(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship, false),
$(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, notEligibleRelationship, false),
$(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship, false),
$(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, notEligibleRelationship, false),
$(eligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, eligibleRelationship, false),
$(eligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, notEligibleRelationship, false),
$(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship, false),
$(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship, false),
$(notEligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship, false),
$(notEligibleLegalEntity, eligibleCounterparty, notEligibleProduct, notEligibleRelationship, false),
$(notEligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship, false),
$(notEligibleLegalEntity, notEligibleCounterparty, eligibleProduct, notEligibleRelationship, false),
$(notEligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, eligibleRelationship, false),
$(notEligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, notEligibleRelationship, false)
);
}
@Test
@Parameters(method = "eligibleParameters")
public void eligibilityShouldBeAsExpected(LegalEntity entity, Counterparty counterparty,
Product product, PartiesRelationship partiesRelationship,
boolean expectedResult) {
(…)
}
Bardzo trudno z powyższego testu odczytać jaki „kontrakt” zawiera klasa testowana ze swoimi kolaboratorami, choć jednocześnie możemy się bronić tym iż jest to bardzo dokładny
kontrakt, w którym wszystko zostało zawarte. Tchnijmy nieco pragmatyzmu do powyższego podejścia:
@Test
public void shouldBeEligibleWhenAllCompoundsAreEligible() { (…) }
private Object[] atLestOneCompoundNotEligible() {
return $(
$(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship),
$(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship),
$(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship),
$(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship)
);
}
@Test
@Parameters(method = "atLestOneCompoundNotEligible")
public void shouldNotBeEligibleWhenAtLeastOneCompoundIsNotEligible(
LegalEntity legalEntity, Counterparty counterparty,
Product product, PartiesRelationship partiesRelationship) { (…) }
W ten sposób zamieniliśmy 16 testów na 5. Zwróćmy ponadto uwagę na to, iż przyrost przypadków testowych w funkcji ilości parametrów wejściowych w pierwszym podejściu jest
geometryczny (z każdym nowym parametrem podwajamy ilość przypadków testowych) natomiast w podejściu drugim jest liniowy (z każdym nowym parametrem dodajemy jeden
nowy test). Poniżej zestawienie liczby przypadków testowych odpowiednio w podejściu „dokładnym” oraz „pragmatycznym” dla określonej liczby parametrów wejściowych:
Liczba parametrów
wejściowych
Liczba przypadków testowych
(podejście dokładne)
Liczba przypadków testowych
(podejście pragmatyczne)
1 2 2
2 4 3
3 8 4
4 16 5
5 32 6
10 1024 11
Zmiana charakterystyki z geometrycznej na liniową wiąże się oczywiście z ceną – tracimy dokładność. Zadajmy sobie jednak pytanie czy jest to autentyczny problem. Poniżej trzy
najważniejsze rzeczy, jakich oczekujemy od testów automatycznych:
1. służą jako testy regresji, mają być naszą siatką bezpieczeństwa przy refaktoryzowaniu kodu
2. dają szybką informację zwrotną
3. powinny jasno oddawać odpowiedzialności klasy, przez co ułatwiać refaktoryzowanie kodu
Ważną rzeczą jest uświadomić sobie, iż w sytuacji gdy mamy dość wysokie pokrycie kodu/wymagań testami, często błędy, które wydają się błędami regresji tak naprawdę są
błędami związanymi ze złym zrozumieniem nowych wymagań. Inżynier implementując nowe wymaganie zmienia również test – co jest normalną praktyką. Przed tego typu błędem
nie uchroni nas ani podejście „dokładne” ani „pragmatyczne”. Pozostają zatem zmiany zrobione przypadkowo, zobaczmy zatem jaką zmianę trzeba wprowadzić aby rozwiązanie
„pragmatyczne” okazało się zbyt liberalne:
Warunek logiczny Rezultat testów
pragmatycznych
Rezultat testów
dokładnych
a && b && c && d && e Ok Ok
A & b && c && d && e Błąd niewykryty Błąd niewykryty
A && b && c && d && e && f Błąd niewykryty Błąd niewykryty
A && c && d && e Błąd wykryty Błąd wykryty
A | b && c && d && e Błąd wykryty Błąd wykryty
A && b && c && d && e || (a && !b) Bład niewykryty Błąd wykryty
Powyższą tabelkę podzieliłem na trzy grupy (pierwszy rząd służy tylko za punkt odniesienia, który pokazuje oryginalny warunek logiczny).
Pierwsza (żółta) część tyczy się błędów, które nie zostaną wykryte w żadnym z podejść:
1. zmiana operatora AND z logicznego na bitowy. W przypadku wartości bool'owskich wynik jest taki sam więc trudno tu mówić o autentycznym błędzie – jest to natomiast
na pewno mylące, narzędzia do statycznej analizy kodu powinny to wychwycić.
2. Dodanie nowego warunku (f) – z testowego punku widzenia jest to dodanie nowej funkcjonalności i powinno wiązać się z dodaniem/zmianą testu tak by tą nową
funkcjonalność pokrywał. Tak jak już wcześniej wspominałem testy automatyczne są testami regresji więc nie jest możliwe aby znalazły tego typu błąd, jeśli tylko zmiana
jest kompatybilna wstecz.
Druga część (zielona) to faktycznie przypadkowe błędy, które możemy zrobić szybko edytując kod (usunięcie jednego z parametrów, zmiana przypadkowa operatora AND na OR,
etc.) - tego typu błędy są wykrywane przez oba testy.
Trzecia część (czerwono-zielona) faktycznie obrazuje jak skomplikowana (wybrałem możliwie najprostszą) musi być (przypadkowa) zmiana w warunku logicznym aby test
pragmatyczny zawiódł. Teraz musimy sobie tylko odpowiedzieć na pytanie jakie jest prawdopodobieństwo wprowadzenia takiej zmiany przypadkowo... i czy chcemy ponosić cenę
całkowitego pokrycia aby się przed nią ustrzec.
d) Studiose – testowanie zbyt wystawne. Dzieje się to w sytuacji, gdy do przetestowania naszej klasy wymagamy szeregu mock'ów, stub'ów etc. Najprostsze rozwiązanie problemu
zbyt dużej liczby mock'ów jest nie stosować ich w ogóle. Podejście funktorowe przychodzi nam z odsieczą. Jego implementacja wszak będzie wymagać refaktoryzowania naszego
kodu. Wykorzystajmy ponownie nasz algorytm do wyznaczania eligibility, wygląda on tak:
Pozostaje pytanie czy potrafimy dokonać takich zmian aby mockowanie nie było potrzebne w powyższej sytuacji. W swojej aktualnej formie EligibilityService łamie zasadę
pojedynczej odpowiedzialności. Wie skąd dostać potrzebne mu informacje (RelationshipService) oraz wie jak wyznaczyć eligibility na podstawie posiadanych informacji. Proponuję
rozdzielić te dwie odpowiedzialności, na część serwisową oraz algorytmiczną. Serwis zbierze dane (bardzo prosta logika, w zasadzie to jej brak – nie bardzo jest co testować) oraz
Algorytm (tu tkwi serce, które musimy należycie przetestować) który te dane przetworzy i wyda odpowiedni werdykt, a serwis następnie zwróci ten werdykt na zewnątrz.
Kolejnym, bardzo prostym sposobem ustrzeżenia się mocków jest wprowadzenie tzw. biblioteki obiektów testowych – obiektów biznesowych będących w odpowiednich stanach.
Jest wiele różnych typów zamienników, które stosujemy w testach acz ich dokładny opis wykracza poza ramy tego artykułu (zainteresowanego czytelnika odsyłam do sieci: Mocks
aren't Stubs by Martin Fowler, Mocks, Fakes, Stubs and Dummies from xUnitPatterns).
Tomasz z akwinu dodał jeszcze jedną ścieżkę do zatracenia w kontekście grzechu obżarstwa.
e) Forente – testowanie niepochamowane, zdziczałe. Dwa punkty które chciałbym zaznaczyć na wstępie tego krótkiego punktu:
– Testy powinny opisywać biznesowe zachowanie klasy tak bardzo jak to tylko możliwe i sprawdzać z możliwie biznesowego punktu widzenia czy zachowanie klasy jest
poprawne
– Testy nie powinny być splątane z kodem (coupled) ponieważ dramatycznie utrudnia to refaktoryzowanie, które jest nieodłącznym aspektem utrzymywania kodu w
czystości.
Łamiemy obie powyższe reguły wykorzystując pewne funkcjonalności framework'ów do mockowania jak np. sprawdzanie kolejności wywołania poszczególnych metod,
sprawdzanie czy dana metoda została wywołana, sprawdzana czy parametry wywołania były odpowiednie. Zwróćmy uwagę, że ani taki test nie odpowiada na pytanie o
odpowiedzialności biznesowe klasy ani nie pomoże w przypadku refaktoringu bo i tak będzie musiał być zmieniony. Zatem po co?
2. Chciwość
Chciwość w świecie testów zazwyczaj manifestuje się w testach systemowych, które pożądają dostępu do zasobów, do których dostępu mieć nie powinny. Zgodnie z ideą BDD test
systemowy, poza tym, że sprawdza czy nasz system zachowuje się w pożądany sposób jest również swego rodzaju biznesową dokumentacją – aby to stwierdzenie było prawdziwe to
taki test musi wykorzystywać tylko interfejsy, które są rozumiane przez ludzi biznesu. Jeśli nagle, by sprawdzić czy system po wykonaniu testu jest w odpowiednim stanie, test sięga
do bazy danych – to jest dokładnie chciwość o której mówię. Jej przyczyną jest to, iż być może system jest nietestowalny i jest to jedyny sposób aby go przetestować ale z drugiej
strony trudno wymagać od użytkowników (którzy z definicji akceptują każdą historyjkę, którą dostarczamy) aby w pełni zrozumieli test akceptacyjny który w połowie wykonania
sięga do bazy danych lub też ów test kończy się stwierdzeniem: „wszystko działa poprawnie, w tabelach users oraz reports są odpowiednie wpisy”....
3. Lenistwo
Testy potrafią być leniwe w dwojaki sposób.
Po pierwsze same zostawiają po sobie bałagan, system w zabrudzonym stanie. Z czasem może się okazać, iż nowo dodany test który przechodzi w izolacji zawsze gdy puszczony na
serwerze ciągłej integracji zaczyna nam migać (czasem przechodzi, czasem nie). Okazuje się że określona kombinacja brudów pozostawiona przez poprzednie testy interferuje z tym
nowo dodanym. Jest to jeden z najtrudniej identyfikowalnych błędów ponieważ może on pojawiać się tylko czasem, w zależności od kolejności wywołania testów poprzedzających
nasz test (kolejności, na którą nie mamy wpływu, zwłaszcza gdy uruchamiamy testy wielowątkowo).
Druga manifestacja lenistwa jest jeszcze gorsza niż poprzednia – celowe wykorzystanie zanieczyszczeń zostawionych przez poprzedni test. Jeśli zdecydujemy się iść tą drogą
musimy sobie zdawać sprawę z tego, że zakładamy sobie na nogi łańcuchy komplikacji. Od tego momentu nie będziemy już w stanie jasno wywnioskować startowego stanu systemu
czytając jeden test, a być może co gorsze tworzymy efekt domina, jeśli jeden test się nie powiedzie, pociągnie za sobą inne testy, które z kolei pociągną kolejne – takie domino
znacząco utrudnia analizę. Kajdany zaczynają również dźwięczeć gdy przyjdzie nam zmienić funkcjonalność (a zatem również test) który „przygotowywał” stan dla jakiegoś innego
testu...
4. Zazdrość
Zazdrość manifestuje się zazwyczaj w testach wyższego poziomu. Generalnie sprowadza się to do zazdroszczenia innym testom, że one mogą coś sprawdzać. Wyobraźmy sobie test
systemowy którego celem jest sprawdzenie czy dla określonej wiadomości wejściowej został wygenerowany raport. W takim teście poza sprawdzeniem czy faktycznie raport został
wygenerowany sprawdzamy jeszcze dokładnie jego zawartość.... czy to dobrze? Dlaczego miałoby to być złe? Pierwszym powodem jest to, że zaciemniamy obraz
odpowiedzialności tego konkretnego testu (problem możemy obejść ukrywając to co faktycznie jest sprawdzane na którymś z niższych poziomów abstrakcji naszego framework'u
testowego). Tego rodzaju sprawdzenia zachęcają do testowania wariacji prowadzących do wygenerowania raportu z innymi wartościami na poziomie testów systemowych – co już
jest poważnym naruszeniem, ponieważ znacząco wydłuży czas trwania testów systemowych.
5. Kenodoxia
W czwartym wieku naszej ery grecki mnich Evagrius Ponticus zdefiniował listę „8 złych myśli”, które to następnie niespełna dwa wieki później papierz Grzegorz I przekuł w „7
śmiertelnych grzechów”. Na liście „siedmiu” Kenodoxia (czy też pycha) nie widnieje (widnieje jedynie duma, choć co ciekawe w katolickim katechiźmie mamy odwortnie, jest
pycha nie ma dumy). Niemniej jednak ze względu na to, iż objawiła się ona ostatnio w testach napisanych przez mój zespół postanowiłem nie pomijać jej w tym artykule. Jedyne
miejsce gdzie test może chwalić się swoimi poczynaniami to logi testowe. Zarówno logi testowe jaki raport z wykonania testów powinny być łatwe do odczytu i analizy. W sytuacji
gdy tak nie jest musimy popracować nad poziomem logowania.
Dwa ostatnie punkty nie tyczą się testów lecz raczej inżyniera, który te testy pisze.
6. Pride
Dobrze jest znać różne języki programowania ale to nie znaczy, że należy ich wszystkich używać w swoich testach czy też generalnie w projekcie. W moim projekcie używamy
trzech języków (java, groovy, xquery, brakuje tylko scali i clojure – acz i takie zapędy były ;-)) i bardzo staramy się aby były jasne granice używania każdego z nich. W testach udało
nam się tą liczbę sprowadzić do dwóch (java, groovy). Każdy język to nowe praktyki kodowania, nowa definicja statycznej analizy kodu, nowe rodzaje błędów do popełnienia.
Bardzo się zastanówcie zanim zrobicie sobie krzywdę wprowadzając drugi język do waszego projektu – wypiszcie sobie konkretne zyski jakie planujecie uzyskać, bo cena jest
wysoka.
7. Wrath/Desire
Zdarzają się chwile, w których już po prostu chcemy żeby ktoś wreszcie z'merge'ował nasze zmiany bo przebyliśmy już dwukrotnie przez „re-basing hell”, a ktoś się czepia.... a to
pokrycia (że tylko 80%), a to nazewnictwa (że niejasne), a to długości metod (że za długie i nadające się do zmiany)......... I to „coś” zaczyna w nas rosnąć rozsadzając zdrowy
rozsądek pozwalając kiełkować sarkazmowi i ironii. W naszym środowisku bardzo dobrze działa parowanie się w takich wypadkach. Po prostu prosisz osobę która robi review, żeby
siadła i napisała ten kawałek z Tobą, robiąc re-review jednocześnie. Wymaga wyjścia spomiędzy krzewów sarkazmu i ironii ale popłaca, ostatecznie wszyscy siedzimy na tym
samym wózku i chcemy żeby ten wózek prowadził się jak najłatwiej i był przynajmniej niebrzydki ;-).

More Related Content

Viewers also liked

lingual orthodontics courses in india /certified fixed orthodontic courses by...
lingual orthodontics courses in india /certified fixed orthodontic courses by...lingual orthodontics courses in india /certified fixed orthodontic courses by...
lingual orthodontics courses in india /certified fixed orthodontic courses by...Indian dental academy
 
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...Amazon Web Services
 
Social Science Assessment Principles
Social Science Assessment PrinciplesSocial Science Assessment Principles
Social Science Assessment PrinciplesAlkhalif Amberol
 
Gastroesophageal Reflux With Relevance To Pediatric Surgery
Gastroesophageal Reflux With Relevance To Pediatric SurgeryGastroesophageal Reflux With Relevance To Pediatric Surgery
Gastroesophageal Reflux With Relevance To Pediatric SurgeryRavi Kanojia
 
75959914 vcads-user-manual-volvo
75959914 vcads-user-manual-volvo75959914 vcads-user-manual-volvo
75959914 vcads-user-manual-volvoluis felipe traipe
 
Magento 101: A technical overview
Magento 101: A technical overviewMagento 101: A technical overview
Magento 101: A technical overviewX.commerce
 
Виховна година на тему « Людина і природа»
Виховна година на тему « Людина і природа»Виховна година на тему « Людина і природа»
Виховна година на тему « Людина і природа»Ковпитська ЗОШ
 
CONSTRUCTION EQUIPMENTS
CONSTRUCTION EQUIPMENTSCONSTRUCTION EQUIPMENTS
CONSTRUCTION EQUIPMENTSshalz_singh
 
Your Secret Weapon: Making Retail With Personality
Your Secret Weapon: Making Retail With PersonalityYour Secret Weapon: Making Retail With Personality
Your Secret Weapon: Making Retail With PersonalityNational Retail Federation
 

Viewers also liked (9)

lingual orthodontics courses in india /certified fixed orthodontic courses by...
lingual orthodontics courses in india /certified fixed orthodontic courses by...lingual orthodontics courses in india /certified fixed orthodontic courses by...
lingual orthodontics courses in india /certified fixed orthodontic courses by...
 
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...
AWS Enterprise Summit London | Shop Direct Leveraging the Cloud for Success i...
 
Social Science Assessment Principles
Social Science Assessment PrinciplesSocial Science Assessment Principles
Social Science Assessment Principles
 
Gastroesophageal Reflux With Relevance To Pediatric Surgery
Gastroesophageal Reflux With Relevance To Pediatric SurgeryGastroesophageal Reflux With Relevance To Pediatric Surgery
Gastroesophageal Reflux With Relevance To Pediatric Surgery
 
75959914 vcads-user-manual-volvo
75959914 vcads-user-manual-volvo75959914 vcads-user-manual-volvo
75959914 vcads-user-manual-volvo
 
Magento 101: A technical overview
Magento 101: A technical overviewMagento 101: A technical overview
Magento 101: A technical overview
 
Виховна година на тему « Людина і природа»
Виховна година на тему « Людина і природа»Виховна година на тему « Людина і природа»
Виховна година на тему « Людина і природа»
 
CONSTRUCTION EQUIPMENTS
CONSTRUCTION EQUIPMENTSCONSTRUCTION EQUIPMENTS
CONSTRUCTION EQUIPMENTS
 
Your Secret Weapon: Making Retail With Personality
Your Secret Weapon: Making Retail With PersonalityYour Secret Weapon: Making Retail With Personality
Your Secret Weapon: Making Retail With Personality
 

Similar to 7 cardinal sins of testing - Artykul

Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...
Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...
Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...The Software House
 
Confitura 2015 - Code Quality Keepers @ Allegro
Confitura 2015 - Code Quality Keepers @ AllegroConfitura 2015 - Code Quality Keepers @ Allegro
Confitura 2015 - Code Quality Keepers @ Allegroallegro.tech
 
Podstawy testowania oprogramowania INCO 2023.pptx
Podstawy testowania oprogramowania INCO 2023.pptxPodstawy testowania oprogramowania INCO 2023.pptx
Podstawy testowania oprogramowania INCO 2023.pptxKatarzyna Javaheri-Szpak
 
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13Więcej testów/mniej kodu - Michał Gaworski, kraQA 13
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13kraqa
 
Jak stworzyć udany system informatyczny
Jak stworzyć udany system informatycznyJak stworzyć udany system informatyczny
Jak stworzyć udany system informatycznyqbeuek
 
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...PMI Szczecin
 
Testy jednostkowe - PHPUnit
Testy jednostkowe - PHPUnitTesty jednostkowe - PHPUnit
Testy jednostkowe - PHPUnitPHPstokPHPstok
 
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...Ziemek Borowski
 
Testowanie w parach - Testwarez 2016
Testowanie w parach - Testwarez 2016Testowanie w parach - Testwarez 2016
Testowanie w parach - Testwarez 2016Damian Szczurek
 
MS - Wprowadzenie do testów jednostkowych
MS - Wprowadzenie do testów jednostkowychMS - Wprowadzenie do testów jednostkowych
MS - Wprowadzenie do testów jednostkowychMarcin Samsonowski
 
Podstawy testowania oprogramowania. Testowanie w praktyce.
Podstawy testowania oprogramowania. Testowanie w praktyce.  Podstawy testowania oprogramowania. Testowanie w praktyce.
Podstawy testowania oprogramowania. Testowanie w praktyce. mamopracuj
 
Unit testing w praktyce... czyli właściwie jak?
Unit testing w praktyce... czyli właściwie jak?Unit testing w praktyce... czyli właściwie jak?
Unit testing w praktyce... czyli właściwie jak?Bartłomiej Cymanowski
 
Tdd - Czyli jak tworzyć dobre jakościowo aplikacje
Tdd - Czyli jak tworzyć dobre jakościowo aplikacjeTdd - Czyli jak tworzyć dobre jakościowo aplikacje
Tdd - Czyli jak tworzyć dobre jakościowo aplikacjeSPARK MEDIA
 

Similar to 7 cardinal sins of testing - Artykul (20)

Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...
Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...
Inżynieria społeczna jako element testów bezpieczeństwa - tylko teoria, czy j...
 
Confitura 2015 - Code Quality Keepers @ Allegro
Confitura 2015 - Code Quality Keepers @ AllegroConfitura 2015 - Code Quality Keepers @ Allegro
Confitura 2015 - Code Quality Keepers @ Allegro
 
Podstawy testowania oprogramowania INCO 2023.pptx
Podstawy testowania oprogramowania INCO 2023.pptxPodstawy testowania oprogramowania INCO 2023.pptx
Podstawy testowania oprogramowania INCO 2023.pptx
 
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13Więcej testów/mniej kodu - Michał Gaworski, kraQA 13
Więcej testów/mniej kodu - Michał Gaworski, kraQA 13
 
Jak stworzyć udany system informatyczny
Jak stworzyć udany system informatycznyJak stworzyć udany system informatyczny
Jak stworzyć udany system informatyczny
 
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...
Krzysztof Moskwa - Podstawy metod zwinnych: jak to działa? Story points, czyl...
 
Testy jednostkowe - PHPUnit
Testy jednostkowe - PHPUnitTesty jednostkowe - PHPUnit
Testy jednostkowe - PHPUnit
 
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...
2008 06-16 pepug-hcl_poznan_-_etykieta_postmastera_czyli_o_uwarunkowaniach_pr...
 
Testowanie w parach - Testwarez 2016
Testowanie w parach - Testwarez 2016Testowanie w parach - Testwarez 2016
Testowanie w parach - Testwarez 2016
 
Kurs "Zrób to tak, aby to zrobić" - prezentacja 4
Kurs "Zrób to tak, aby to zrobić" - prezentacja 4Kurs "Zrób to tak, aby to zrobić" - prezentacja 4
Kurs "Zrób to tak, aby to zrobić" - prezentacja 4
 
JMeter - narzędzie testera - notatki
JMeter - narzędzie testera - notatkiJMeter - narzędzie testera - notatki
JMeter - narzędzie testera - notatki
 
MS - Wprowadzenie do testów jednostkowych
MS - Wprowadzenie do testów jednostkowychMS - Wprowadzenie do testów jednostkowych
MS - Wprowadzenie do testów jednostkowych
 
Podstawy testowania oprogramowania. Testowanie w praktyce.
Podstawy testowania oprogramowania. Testowanie w praktyce.  Podstawy testowania oprogramowania. Testowanie w praktyce.
Podstawy testowania oprogramowania. Testowanie w praktyce.
 
Kurs "Zrób to tak, aby to zrobić" - prezentacja 6
Kurs "Zrób to tak, aby to zrobić" - prezentacja 6Kurs "Zrób to tak, aby to zrobić" - prezentacja 6
Kurs "Zrób to tak, aby to zrobić" - prezentacja 6
 
Kwestionowanie ISTQB
Kwestionowanie ISTQBKwestionowanie ISTQB
Kwestionowanie ISTQB
 
Wprowadzenie do PHPUnit
Wprowadzenie do PHPUnitWprowadzenie do PHPUnit
Wprowadzenie do PHPUnit
 
Unit testing w praktyce... czyli właściwie jak?
Unit testing w praktyce... czyli właściwie jak?Unit testing w praktyce... czyli właściwie jak?
Unit testing w praktyce... czyli właściwie jak?
 
Testowanie automatyczne 2024 INCO Academy
Testowanie automatyczne 2024 INCO AcademyTestowanie automatyczne 2024 INCO Academy
Testowanie automatyczne 2024 INCO Academy
 
university day 1
university day 1university day 1
university day 1
 
Tdd - Czyli jak tworzyć dobre jakościowo aplikacje
Tdd - Czyli jak tworzyć dobre jakościowo aplikacjeTdd - Czyli jak tworzyć dobre jakościowo aplikacje
Tdd - Czyli jak tworzyć dobre jakościowo aplikacje
 

7 cardinal sins of testing - Artykul

  • 1. Wiele zostało już napisane na temat anty-wzorców jakie obserwujemy w świecie testów automatycznych. James Carr i jego „TDD Anti-Patterns” to lektura obowiązkową dla każdego, kto chce uniknąć podstawowych błędów. W tym artykule chciałem się skupić na błędach mniej oczywistych, decyzjach, które w momencie podejmowania wydają się rozsądne lub też zupełnie niegroźne, a które prowadzą do nieoczekiwanych rezultatów. Całość ujęta jest w ramy siedmiu grzechów głównych choć jak się przekonacie błędów (podobnie jak grzechów) jest znacznie więcej. 1. Obżarstwo Już papież Grzegorz I wyszczególnił 5 dróg, które wiodą do popełnienia grzechu obżarstwa... Ja podążę tym samym tropem. a) Praepropare – testowanie zbyt wczesne. Wyobraźmy sobie system, którego rolą jest wysyłanie raportów w oparciu o wiadomości wejściowe. Raport zawiera w sobie identyfikator wiadomości wejściowej, która jest jego katalizatorem. Operacja wysyłania raportów jest oczywiście asynchroniczna względem wykonania testu, toteż musimy poczekać na odpowiedź. Test możemy skonstruować zatem na dwa sposoby: @Test public void shouldGenerateReportCorrespondingToInboundMessage() { InboundMessage message = new InboundMessage(id, legalEntity); sender.send(message); await().atMost(TEN_SECONDS).until(appropriateReportIsReceived(message)); } private Runnable appropriateReportIsReceived(InboundMessage message) { return () -> reportListener.listenFor(aReport() .thatHasId(message.getId()) .thatIsAgainstLegalEntity(message.getLegalEntity()) ); } @Test public void shouldGenerateReportCorrespondingToInboundMessage_2() { InboundMessage message = new InboundMessage(id, legalEntity); sender.send(message); await().atMost(TEN_SECONDS).until(correspondingReportIsReceived(message)); Report report = reportListener.getReport(id); assertThat(report, is(aReport().thatIsAgainstLegalEntity(legalEntity))); } private Runnable correspondingReportIsReceived(InboundMessage message) { return () -> reportListener.listenFor(aReport().thatHasId(message.getId()));}
  • 2. Pierwsze podejście jest nieco bardziej czytelne aniżeli podejście drugie, zatem zadajmy sobie pytanie czy jest powód dla którego poświęcilibyśmy czytelność testu. Jednym z głównych powodów dla których inwestujemy w automatyzację testów jest szybkość z jaką uzyskujemy informację zwrotną. Rozważmy raport wygenerowany dla danej wiadomości wejściowej w sytuacji gdy posiada on złe legalEntity: I) w podejściu pierwszym o tym fakcie dowiemy się dopiero po upływie zadeklarowanego czasu oczekiwania (w naszym przypadku 10s). Dodatkowo informacja o błędzie będzie bardzo ogólna, dowiemy się jedynie iż raport o zadanym id oraz legalEntity nie pojawił się w oczekiwanym czasie – zwróćmy uwagę na to, iż jest to informacja nie tylko zbyt ogólna ale co gorsze wprowadzająca w błąd naszą intuicję. II) W podejściu drugim o błędzie dowiemy się zaraz po otrzymaniu raportu i będzie on bardzo dokładnie zidentyfikowany Dodatkowo czytelność drugiego rozwiązania możemy nieco poprawić enkapsulując oczekiwanie na raport w klasie ReportListener. b) Laute – testowanie zbyt drogie. Ogromna bolączka zwinnych testów, choroba którą zwykliśmy diagnozować dopiero, gdy jest już w bardzo zaawansowanym stadium. Dobrą regułą, która pozwoli nam szybko poczuć iż choroba się właśnie zaczęła, lecz również pozwoli nam wyciągnąć maksimum korzyści z inwestycji w testy automatyczne, jest uruchamianie kompletnego zbioru testów po każdej zmianie kodu źródłowego. Musimy sobie zdawać sprawę z tego iż często optymalizacja szybkości wykonania testu może pociągać za sobą zmianę kodu produkcyjnego. I) Pierwszą, najważniejszą regułą optymalizacji kosztów testowania to umiejscowienie testu na odpowiednim poziomie automatycznej piramidy testów, przy czym złota reguła piramidy mówi: im niżej tym taniej. Oczywistością jest, że nie każdą funkcjonalność będziemy w stanie przetestować na poziomie testów jednostkowych. II) Wyobraźmy sobie iż nasza aplikacja wykonuje jakąś czynność periodycznie. Zaimplementowaliśmy scheduler który odpala się zgodnie z cron'owym harmonogramem. Aby sensownie integracyjnie przetestować taką funkcjonalność możemy zbudować część kontekstu dookoła klasy wyzwalanej przez scheduler (bez samej funkcjonalności automatycznego odpalania) i uruchomić jej odpowiednią metodę publiczną. Co możemy jednak zrobić gdy chcemy przetestować integrację tej funkcjonalności z pozostałymi modułami naszego systemu? Zmienić kod produkcyjny w dwojaki sposób: 1. dodać interfejs (jmx, rest, rmi, etc.) pozwalający na uruchomienie owej funkcjonalności w trybie „na żądanie” 2. dodać możliwość wyłączenia scheduler'ów. III) Wyobraźmy sobie iż nasz system wysyła raporty dla określonych wiadomości wejściowych, acz są też takie wiadomości dla których nasz system nie wysyła żadnych raportów... jak automatycznie przetestować fakt, iż raport nie został wysłany? Najprostszy, brutalny sposób to oczywiście zaimplementować timeout w naszym teście, przez co domyślnie stwierdzamy „jeśli nie zdarzyło się do tej pory to znaczy, że się już nie zdarzy”. Taki test szkodzi całemu zbiorowi testów w dwojaki sposób: 1. znacząco wydłuża czas trwania testów (za każdym razem musimy czekać cały timeout) 2. potencjalnie wprowadza „migotanie testów” (test czasem przejdzie, czasem nie). Wyobraźmy sobie prostą sytuację gdy nasz serwer ciągłej integracji jest bardzo obciążony i nagle nasz system zaczyna zwalniać, przez co dla zadanej wiadomości wejściowej system wyśle błędnie raport ale zazwyczaj dopiero po timeout'cie i tylko czasem uda mu się wysłać go szybciej. W ten sposób ludzie przyzwyczają się, że jest jeden test który „migocze” więc.... trzeba go puścić jeszcze raz bo przecież zmiany z czerwonym testem nikt nam nie pozwoli włączyć do master'a/trunk'a. Odpowiedzią na przedstawiony problem jest kolejna złota reguła testowania automatycznego: nie można udowodnić/przetestować że coś się nie stało (musielibyśmy czekać nieskończenie długo.... a to trochę za długo). Jedynym sensownym sposobem na wybrnięcie z tego impasu jest przetestować, że coś się stało ;-). To może wymagać sporych zmian w samym systemie (celem jest poprawienie testowalności). Jakkolwiek skomplikowane technicznie by to nie było, koncepcyjnie sprowadza się do bardzo prostej rzeczy, test musi wiedzieć iż procesowanie danej wiadomości się skończyło i jest jakiś marker na, który musi czekać.
  • 3. c) Nimis /Ardenter – testowanie w zbyt dużej ilości , zbyt dokładne. Intuicyjnie może się nam wydawać, że im więcej przetestujemy tym lepiej... nic bardziej mylnego. Zrobimy sobie potencjalnie krzywdę na trzy sposoby: I) tracimy czytelność kontraktu jaki nasza klasa zawiera ze swoimi kolaboratorami II) tracimy czas na utrzymywanie większej ilości kodu III) wydłużamy sobie czas trwania naszego zbioru testów. Jeśli dopuścimy się takiej nierozwagi na poziomie jednostkowym to owe wydłużenie będzie zazwyczaj marginalne, lecz wystarczy wejść po stopniach piramidy testów nieco wyżej by czas oczekiwania na testy znacząco się wydłużył. Co to zatem znaczy „zbyt dużo” i jak się przed tym uchronić? Stare, dobre praktyki wypracowane w czasach intensywnych testów manualnych przychodzą nam z pomocą: klasy równoważności, wartości brzegowe, pokrycie diagramu przepływów, tabele decyzyjne. Opis tych metod wykracza poza ramy tego artykułu ale też jest dużo dokumentacji na ten temat w sieci. Chciałbym się skupić na czymś czego w sieci jeszcze nie znalazłem otóż na sposobie poradzenia sobie z testami automatycznymi skomplikowanych warunków logicznych. Z jednej strony nic prostszego jak użyć testów parametrycznych i przetestować cały zbiór kombinacji... z drugiej strony będąc tak dokładni całkowicie zaciemniamy obraz odpowiedzialności naszej klasy. Musimy znaleźć zatem jakiś sensowny kompromis. Rozważmy następującą w sumie dość prostą implementację która bazuje decyzję na podstawie 4 parametrów: public boolean isEligible(InboundMessage message) { LegalEntity legalEntity = message.getLegalEntity(); Counterparty counterparty = message.getCounterparty(); PartiesRelation partiesRelation = relationService.determineRelation(legalEntity, counterparty); Product product = message.getProduct(); return legalEntity.isEligible() && counterparty.isEligible() && partiesRelationship.isEligible() && product.isEligible( } By dokładnie przetestować powyższy kod musimy pokryć 16 (24 ) przypadków: public Object[] eligibleParameters() { return $( $(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship, true), $(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship, false), $(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship, false), $(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, notEligibleRelationship, false), $(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship, false), $(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, notEligibleRelationship, false), $(eligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, eligibleRelationship, false), $(eligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, notEligibleRelationship, false), $(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship, false), $(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship, false),
  • 4. $(notEligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship, false), $(notEligibleLegalEntity, eligibleCounterparty, notEligibleProduct, notEligibleRelationship, false), $(notEligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship, false), $(notEligibleLegalEntity, notEligibleCounterparty, eligibleProduct, notEligibleRelationship, false), $(notEligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, eligibleRelationship, false), $(notEligibleLegalEntity, notEligibleCounterparty, notEligibleProduct, notEligibleRelationship, false) ); } @Test @Parameters(method = "eligibleParameters") public void eligibilityShouldBeAsExpected(LegalEntity entity, Counterparty counterparty, Product product, PartiesRelationship partiesRelationship, boolean expectedResult) { (…) } Bardzo trudno z powyższego testu odczytać jaki „kontrakt” zawiera klasa testowana ze swoimi kolaboratorami, choć jednocześnie możemy się bronić tym iż jest to bardzo dokładny kontrakt, w którym wszystko zostało zawarte. Tchnijmy nieco pragmatyzmu do powyższego podejścia: @Test public void shouldBeEligibleWhenAllCompoundsAreEligible() { (…) } private Object[] atLestOneCompoundNotEligible() { return $( $(eligibleLegalEntity, eligibleCounterparty, eligibleProduct, notEligibleRelationship), $(eligibleLegalEntity, eligibleCounterparty, notEligibleProduct, eligibleRelationship), $(eligibleLegalEntity, notEligibleCounterparty, eligibleProduct, eligibleRelationship), $(notEligibleLegalEntity, eligibleCounterparty, eligibleProduct, eligibleRelationship) ); } @Test @Parameters(method = "atLestOneCompoundNotEligible") public void shouldNotBeEligibleWhenAtLeastOneCompoundIsNotEligible( LegalEntity legalEntity, Counterparty counterparty, Product product, PartiesRelationship partiesRelationship) { (…) } W ten sposób zamieniliśmy 16 testów na 5. Zwróćmy ponadto uwagę na to, iż przyrost przypadków testowych w funkcji ilości parametrów wejściowych w pierwszym podejściu jest
  • 5. geometryczny (z każdym nowym parametrem podwajamy ilość przypadków testowych) natomiast w podejściu drugim jest liniowy (z każdym nowym parametrem dodajemy jeden nowy test). Poniżej zestawienie liczby przypadków testowych odpowiednio w podejściu „dokładnym” oraz „pragmatycznym” dla określonej liczby parametrów wejściowych: Liczba parametrów wejściowych Liczba przypadków testowych (podejście dokładne) Liczba przypadków testowych (podejście pragmatyczne) 1 2 2 2 4 3 3 8 4 4 16 5 5 32 6 10 1024 11 Zmiana charakterystyki z geometrycznej na liniową wiąże się oczywiście z ceną – tracimy dokładność. Zadajmy sobie jednak pytanie czy jest to autentyczny problem. Poniżej trzy najważniejsze rzeczy, jakich oczekujemy od testów automatycznych: 1. służą jako testy regresji, mają być naszą siatką bezpieczeństwa przy refaktoryzowaniu kodu 2. dają szybką informację zwrotną 3. powinny jasno oddawać odpowiedzialności klasy, przez co ułatwiać refaktoryzowanie kodu Ważną rzeczą jest uświadomić sobie, iż w sytuacji gdy mamy dość wysokie pokrycie kodu/wymagań testami, często błędy, które wydają się błędami regresji tak naprawdę są błędami związanymi ze złym zrozumieniem nowych wymagań. Inżynier implementując nowe wymaganie zmienia również test – co jest normalną praktyką. Przed tego typu błędem nie uchroni nas ani podejście „dokładne” ani „pragmatyczne”. Pozostają zatem zmiany zrobione przypadkowo, zobaczmy zatem jaką zmianę trzeba wprowadzić aby rozwiązanie „pragmatyczne” okazało się zbyt liberalne: Warunek logiczny Rezultat testów pragmatycznych Rezultat testów dokładnych a && b && c && d && e Ok Ok A & b && c && d && e Błąd niewykryty Błąd niewykryty A && b && c && d && e && f Błąd niewykryty Błąd niewykryty A && c && d && e Błąd wykryty Błąd wykryty A | b && c && d && e Błąd wykryty Błąd wykryty A && b && c && d && e || (a && !b) Bład niewykryty Błąd wykryty Powyższą tabelkę podzieliłem na trzy grupy (pierwszy rząd służy tylko za punkt odniesienia, który pokazuje oryginalny warunek logiczny). Pierwsza (żółta) część tyczy się błędów, które nie zostaną wykryte w żadnym z podejść: 1. zmiana operatora AND z logicznego na bitowy. W przypadku wartości bool'owskich wynik jest taki sam więc trudno tu mówić o autentycznym błędzie – jest to natomiast
  • 6. na pewno mylące, narzędzia do statycznej analizy kodu powinny to wychwycić. 2. Dodanie nowego warunku (f) – z testowego punku widzenia jest to dodanie nowej funkcjonalności i powinno wiązać się z dodaniem/zmianą testu tak by tą nową funkcjonalność pokrywał. Tak jak już wcześniej wspominałem testy automatyczne są testami regresji więc nie jest możliwe aby znalazły tego typu błąd, jeśli tylko zmiana jest kompatybilna wstecz. Druga część (zielona) to faktycznie przypadkowe błędy, które możemy zrobić szybko edytując kod (usunięcie jednego z parametrów, zmiana przypadkowa operatora AND na OR, etc.) - tego typu błędy są wykrywane przez oba testy. Trzecia część (czerwono-zielona) faktycznie obrazuje jak skomplikowana (wybrałem możliwie najprostszą) musi być (przypadkowa) zmiana w warunku logicznym aby test pragmatyczny zawiódł. Teraz musimy sobie tylko odpowiedzieć na pytanie jakie jest prawdopodobieństwo wprowadzenia takiej zmiany przypadkowo... i czy chcemy ponosić cenę całkowitego pokrycia aby się przed nią ustrzec. d) Studiose – testowanie zbyt wystawne. Dzieje się to w sytuacji, gdy do przetestowania naszej klasy wymagamy szeregu mock'ów, stub'ów etc. Najprostsze rozwiązanie problemu zbyt dużej liczby mock'ów jest nie stosować ich w ogóle. Podejście funktorowe przychodzi nam z odsieczą. Jego implementacja wszak będzie wymagać refaktoryzowania naszego kodu. Wykorzystajmy ponownie nasz algorytm do wyznaczania eligibility, wygląda on tak: Pozostaje pytanie czy potrafimy dokonać takich zmian aby mockowanie nie było potrzebne w powyższej sytuacji. W swojej aktualnej formie EligibilityService łamie zasadę pojedynczej odpowiedzialności. Wie skąd dostać potrzebne mu informacje (RelationshipService) oraz wie jak wyznaczyć eligibility na podstawie posiadanych informacji. Proponuję rozdzielić te dwie odpowiedzialności, na część serwisową oraz algorytmiczną. Serwis zbierze dane (bardzo prosta logika, w zasadzie to jej brak – nie bardzo jest co testować) oraz Algorytm (tu tkwi serce, które musimy należycie przetestować) który te dane przetworzy i wyda odpowiedni werdykt, a serwis następnie zwróci ten werdykt na zewnątrz.
  • 7. Kolejnym, bardzo prostym sposobem ustrzeżenia się mocków jest wprowadzenie tzw. biblioteki obiektów testowych – obiektów biznesowych będących w odpowiednich stanach. Jest wiele różnych typów zamienników, które stosujemy w testach acz ich dokładny opis wykracza poza ramy tego artykułu (zainteresowanego czytelnika odsyłam do sieci: Mocks aren't Stubs by Martin Fowler, Mocks, Fakes, Stubs and Dummies from xUnitPatterns). Tomasz z akwinu dodał jeszcze jedną ścieżkę do zatracenia w kontekście grzechu obżarstwa. e) Forente – testowanie niepochamowane, zdziczałe. Dwa punkty które chciałbym zaznaczyć na wstępie tego krótkiego punktu: – Testy powinny opisywać biznesowe zachowanie klasy tak bardzo jak to tylko możliwe i sprawdzać z możliwie biznesowego punktu widzenia czy zachowanie klasy jest poprawne – Testy nie powinny być splątane z kodem (coupled) ponieważ dramatycznie utrudnia to refaktoryzowanie, które jest nieodłącznym aspektem utrzymywania kodu w czystości. Łamiemy obie powyższe reguły wykorzystując pewne funkcjonalności framework'ów do mockowania jak np. sprawdzanie kolejności wywołania poszczególnych metod, sprawdzanie czy dana metoda została wywołana, sprawdzana czy parametry wywołania były odpowiednie. Zwróćmy uwagę, że ani taki test nie odpowiada na pytanie o odpowiedzialności biznesowe klasy ani nie pomoże w przypadku refaktoringu bo i tak będzie musiał być zmieniony. Zatem po co? 2. Chciwość Chciwość w świecie testów zazwyczaj manifestuje się w testach systemowych, które pożądają dostępu do zasobów, do których dostępu mieć nie powinny. Zgodnie z ideą BDD test systemowy, poza tym, że sprawdza czy nasz system zachowuje się w pożądany sposób jest również swego rodzaju biznesową dokumentacją – aby to stwierdzenie było prawdziwe to taki test musi wykorzystywać tylko interfejsy, które są rozumiane przez ludzi biznesu. Jeśli nagle, by sprawdzić czy system po wykonaniu testu jest w odpowiednim stanie, test sięga do bazy danych – to jest dokładnie chciwość o której mówię. Jej przyczyną jest to, iż być może system jest nietestowalny i jest to jedyny sposób aby go przetestować ale z drugiej strony trudno wymagać od użytkowników (którzy z definicji akceptują każdą historyjkę, którą dostarczamy) aby w pełni zrozumieli test akceptacyjny który w połowie wykonania sięga do bazy danych lub też ów test kończy się stwierdzeniem: „wszystko działa poprawnie, w tabelach users oraz reports są odpowiednie wpisy”....
  • 8. 3. Lenistwo Testy potrafią być leniwe w dwojaki sposób. Po pierwsze same zostawiają po sobie bałagan, system w zabrudzonym stanie. Z czasem może się okazać, iż nowo dodany test który przechodzi w izolacji zawsze gdy puszczony na serwerze ciągłej integracji zaczyna nam migać (czasem przechodzi, czasem nie). Okazuje się że określona kombinacja brudów pozostawiona przez poprzednie testy interferuje z tym nowo dodanym. Jest to jeden z najtrudniej identyfikowalnych błędów ponieważ może on pojawiać się tylko czasem, w zależności od kolejności wywołania testów poprzedzających nasz test (kolejności, na którą nie mamy wpływu, zwłaszcza gdy uruchamiamy testy wielowątkowo). Druga manifestacja lenistwa jest jeszcze gorsza niż poprzednia – celowe wykorzystanie zanieczyszczeń zostawionych przez poprzedni test. Jeśli zdecydujemy się iść tą drogą musimy sobie zdawać sprawę z tego, że zakładamy sobie na nogi łańcuchy komplikacji. Od tego momentu nie będziemy już w stanie jasno wywnioskować startowego stanu systemu czytając jeden test, a być może co gorsze tworzymy efekt domina, jeśli jeden test się nie powiedzie, pociągnie za sobą inne testy, które z kolei pociągną kolejne – takie domino znacząco utrudnia analizę. Kajdany zaczynają również dźwięczeć gdy przyjdzie nam zmienić funkcjonalność (a zatem również test) który „przygotowywał” stan dla jakiegoś innego testu... 4. Zazdrość Zazdrość manifestuje się zazwyczaj w testach wyższego poziomu. Generalnie sprowadza się to do zazdroszczenia innym testom, że one mogą coś sprawdzać. Wyobraźmy sobie test systemowy którego celem jest sprawdzenie czy dla określonej wiadomości wejściowej został wygenerowany raport. W takim teście poza sprawdzeniem czy faktycznie raport został wygenerowany sprawdzamy jeszcze dokładnie jego zawartość.... czy to dobrze? Dlaczego miałoby to być złe? Pierwszym powodem jest to, że zaciemniamy obraz odpowiedzialności tego konkretnego testu (problem możemy obejść ukrywając to co faktycznie jest sprawdzane na którymś z niższych poziomów abstrakcji naszego framework'u testowego). Tego rodzaju sprawdzenia zachęcają do testowania wariacji prowadzących do wygenerowania raportu z innymi wartościami na poziomie testów systemowych – co już jest poważnym naruszeniem, ponieważ znacząco wydłuży czas trwania testów systemowych. 5. Kenodoxia W czwartym wieku naszej ery grecki mnich Evagrius Ponticus zdefiniował listę „8 złych myśli”, które to następnie niespełna dwa wieki później papierz Grzegorz I przekuł w „7 śmiertelnych grzechów”. Na liście „siedmiu” Kenodoxia (czy też pycha) nie widnieje (widnieje jedynie duma, choć co ciekawe w katolickim katechiźmie mamy odwortnie, jest pycha nie ma dumy). Niemniej jednak ze względu na to, iż objawiła się ona ostatnio w testach napisanych przez mój zespół postanowiłem nie pomijać jej w tym artykule. Jedyne miejsce gdzie test może chwalić się swoimi poczynaniami to logi testowe. Zarówno logi testowe jaki raport z wykonania testów powinny być łatwe do odczytu i analizy. W sytuacji gdy tak nie jest musimy popracować nad poziomem logowania. Dwa ostatnie punkty nie tyczą się testów lecz raczej inżyniera, który te testy pisze. 6. Pride Dobrze jest znać różne języki programowania ale to nie znaczy, że należy ich wszystkich używać w swoich testach czy też generalnie w projekcie. W moim projekcie używamy trzech języków (java, groovy, xquery, brakuje tylko scali i clojure – acz i takie zapędy były ;-)) i bardzo staramy się aby były jasne granice używania każdego z nich. W testach udało nam się tą liczbę sprowadzić do dwóch (java, groovy). Każdy język to nowe praktyki kodowania, nowa definicja statycznej analizy kodu, nowe rodzaje błędów do popełnienia. Bardzo się zastanówcie zanim zrobicie sobie krzywdę wprowadzając drugi język do waszego projektu – wypiszcie sobie konkretne zyski jakie planujecie uzyskać, bo cena jest wysoka. 7. Wrath/Desire Zdarzają się chwile, w których już po prostu chcemy żeby ktoś wreszcie z'merge'ował nasze zmiany bo przebyliśmy już dwukrotnie przez „re-basing hell”, a ktoś się czepia.... a to pokrycia (że tylko 80%), a to nazewnictwa (że niejasne), a to długości metod (że za długie i nadające się do zmiany)......... I to „coś” zaczyna w nas rosnąć rozsadzając zdrowy rozsądek pozwalając kiełkować sarkazmowi i ironii. W naszym środowisku bardzo dobrze działa parowanie się w takich wypadkach. Po prostu prosisz osobę która robi review, żeby siadła i napisała ten kawałek z Tobą, robiąc re-review jednocześnie. Wymaga wyjścia spomiędzy krzewów sarkazmu i ironii ale popłaca, ostatecznie wszyscy siedzimy na tym samym wózku i chcemy żeby ten wózek prowadził się jak najłatwiej i był przynajmniej niebrzydki ;-).