Dane oryginału

Original edition copyright © 2018 by Arnon Axelrod. All rights reserved. Title of English-language original: Complete Guide to Test Automation: Techniques, Practices, and Patterns for Building and Maintaining Effective Software Projects by Arnon Axelrod, ISBN 978-1-48423-831-8, published by Apress. Polish-language edition copyright © 2019 by Polish Scientific Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All rights reserved.

Przekład Krzysztof Kapustka na zlecenie WITKOM Witold Sikorski

Projekt okładki polskiego wydania Joanna Andryjowicz

Wydawca Edyta Kawala

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Małgorzata Dąbkowska-Kowalik

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN: Marcin Kośka/Woblink

Konsultacja merytoryczna

dr hab. Adam Roman, Uniwersytet Jagielloński

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2019

ISBN 978-83-01-20854-7

eBook został przygotowany na podstawie wydania papierowego z 2019 r., (wyd. I)
Warszawa 2019

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl, reklama@pwn.pl

www.pwn.pl

Pamięci moich zmarłych dziadków,
Nathana i Lei Axelrod, pionierów izraelskiego kina.
Pamięć o Was jest moją inspiracją.

O autorze

Arnon Axelrod jest ekspertem w dziedzinie automatyzacji testów i pracuje jako starszy konsultant, architekt, szkoleniowiec oraz lider zespołu automatyzacji w Sela Group. W wieku 10 lat Arnon zaczął programować swój komputer ZX-Spectrum i od tamtej pory nigdy nie zatracił pasji do programowania.

Po uzyskaniu w 1999 roku tytułu licencjata z matematyki i informatyki na Uniwersytecie Ben-Guriona, Arnon rozpoczął pracę w korporacji Microsoft na stanowisku inżyniera testów oprogramowania, gdzie po raz pierwszy zetknął się z zagadnieniem automatyzacji testów. Od tamtej pory pracował w kilku nowoczesnych firmach, głównie jako inżynier oprogramowania, aż po dzień, w którym ponownie odkrył automatyzację testów z zupełnie nowej perspektywy. W 2010 roku, po kilku latach korzystania z metodyki Agile, Arnon – pracując wówczas w firmie Retalix (zakupionej później przez NCR Corporation) – zdał sobie sprawę, że efektywna automatyzacja testów, a dokładniej technika tworzenia oprogramowania sterowanego testami akceptacyjnymi (ATDD), jest niezbędna do błyskawicznego i efektywnego dostarczania wysokiej jakości oprogramowania. Podczas pracy w NCR Arnon zaprojektował infrastrukturę do automatyzacji testów, która była stosowana przez ponad 100 deweloperów i umożliwiała wykonywanie ponad 4000 testów akceptacyjnych w mniej niż 20 minut.

W 2015 roku Arnon dołączył do Sela Group, gdzie pracuje obecnie, a jego misją jest popularyzowanie swojej wiedzy wśród jak największej liczby firm i osób indywidualnych, aby pomóc im efektywniej tworzyć wysokiej jakości oprogramowanie poprzez właściwe wykorzystywanie automatyzacji testów.

W wolnym czasie Arnon lubi żeglować, grać na fortepianie i śpiewać w chórze. Mieszka w mieście Matan w Izraelu wraz ze swoją żoną, Osnat, oraz ich trzema synami: Orim, Eladem i Avivem.

Arnona Axelroda można obserwować na LinkedIn, czytać jego blog pod adresem http://blogs.microsoft.co.il/arnona/ lub też skontaktować się z nim bezpośrednio pod adresem arnonaxelrod@hotmail.com.

O recenzencie technicznym

Bas Dijkstra jest konsultantem oraz szkoleniowcem w zakresie testowania i automatyzacji. Specjalizuje się w tworzeniu oraz implementowaniu strategii automatyzacji wspierających testowanie – zaczynając od udzielania odpowiedzi na pytanie „dlaczego” automatyzacja, aż po samo pisanie efektywnych rozwiązań automatyzacji.

Bas prowadzi szkolenia na różne tematy związane z automatyzacją. Ponadto regularnie publikuje posty blogowe i artykuły poświęcone różnym tematom związanym z automatyzacją testów – zarówno na swojej własnej stronie (https://www.ontestautomation.com/), jak również w innych witrynach i magazynach branżowych.

Podziękowania

Przede wszystkim chciałbym podziękować mojej żonie Osnat – ta książka nie byłaby możliwa bez ogromnego wsparcia, jakie od Ciebie otrzymałem, a wiem, że nie było to proste! Choć robiłem co w mojej mocy, aby praca nad tą książką nie miała wpływu na nasze życie prywatne, to jednak pozostawiałem Cię samotną przez wiele długich wieczorów i zrzucałem na Ciebie więcej domowych obowiązków niż zazwyczaj. Nie sądzę, że w ten sposób uda mi się Ci to jakoś wynagrodzić, ale chcę Ci powiedzieć jedną rzecz: kocham Cię!

Następnie chciałbym bardzo podziękować mojemu bezpośredniemu menedżerowi i szefowi działu „DevOps and Automation” w firmie Sela, Shmulikowi Segalowi, który również wspierał mnie w tej pracy i pozwolił mi poświęcić cenny czas na pracę nad tą książką, pomimo że nie miało to żadnego ekonomicznego uzasadnienia. Shmulik, pomijając już Twoje wsparcie przy tworzeniu tej książki, dziękuję Ci jako menedżerowi i człowiekowi. Dajesz mi siłę, która pozwala mi sięgać szczytów w mojej karierze, na które nigdy nie myślałem, że będę w stanie się wznieść. A wszystko to robisz w sposób bardzo sympatyczny.

Chcę również podziękować Sashy Goldshteinowi, byłemu CTO w firmie Sela (autorowi książki Pro .NET Performance wydawnictwa Apress z 2012 roku oraz współautorowi Introducing Windows 7 for Developers od Microsoft Press z 2011 roku), który próbował odwieść mnie od pisania tej książki, ale jak widać to mu się udało. Miałeś rację przynajmniej w jednym: napisanie jej zajęło mi znacznie więcej czasu niż planowałem. Niemniej jednak znacząco mi pomogłeś i doradziłeś, w tym również zachęciłeś mnie do wysłania propozycji na książkę do wydawnictwa Apress.

Chciałbym także podziękować Zoharowi Lavy’emu z Seli, który koordynuje mój harmonogram i pomaga mi przy wielu zadaniach administracyjnych – praca z Tobą to prawdziwa przyjemność! Dziękuję całej załodze administracyjnej w Sela za całą istotną i ciężką pracę, jaką wykonujecie za kulisami, a także moim przełożonym i właścicielom Seli: dyrektorowi naczelnemu, Davidowi Basa’owi, prezesowi Sela College, Caro Segalowi oraz wiceprezesowi ds. operacji globalnych, Ishai Ramowi, za przewodnictwo Seli i uczynienie z niej tak wspaniałego miejsca do pracy. I w końcu dziękuję wszystkim moim utalentowanym współpracownikom – od każdego z Was sporo się nauczyłem.

Dziękuję Carlowi Franklinowi i Richardowi Campbellowi, prowadzącym podcast „.NET Rocks”, za poszerzanie moich horyzontów, rozbawianie mnie i sprawianie, że moja droga do pracy jest dużo bardziej przyjemna. Carl, dziękuję również za stworzenie kolekcji „muzyki do słuchania podczas kodowania”, która pomogła mi skupić się podczas pracy nad tą książką.

Muszę również podziękować wszystkim ludziom, dzięki którym książka ta nabrała właściwego kształtu: przede wszystkim Basowi Dijkstrze, mojemu świetnemu i wysoce profesjonalnemu recenzentowi technicznemu, za uważne przeczytanie każdego zdania i dostarczenie wielu cennych uwag, wniosków i sugestii, które pozwoliły mi ulepszyć tę książkę. Bez Ciebie ta książka byłaby prawdopodobnie nic nie warta…

Na koniec chcę także podziękować całemu zespołowi redaktorskiemu w Apress: Ricie Fernando Kim, mojej redaktorce koordynującej, za zarządzanie postępem moich prac i dostarczanie cennych wskazówek i porad odnośnie wszystkiego, o co pytałem lub powinienem zapytać. Laurze C. Berendson, redaktor prowadzącej, za pomoc w ukształtowaniu i zaprezentowaniu moich pomysłów w najlepszy możliwy sposób; Shivangi (Shiva) Ramachandranowi, redaktorowi, za zarządzanie tym projektem, a także Susan McDermott, starszej redaktorce, za zaakceptowanie mojej propozycji na tę książkę i przede wszystkim za wiarę we mnie. Dziękuję Wam wszystkim!

Wprowadzenie

Istnieje wiele świetnych książek na temat automatyzacji testów, a w szczególności na temat najlepszych praktyk w tym zakresie. Jednak żadna z tych książek nie jest uniwersalna. Jak to ktoś kiedyś powiedział: „Te ‚najlepsze praktyki’ są zawsze kontekstowe: nawet coś tak powszechnego jak oddychanie może mieć katastrofalne skutki, jeśli kontekstem będzie swobodne nurkowanie…”.

Większość książek, które przeczytałem do tej pory na temat automatyzacji testów, skierowana jest w dużej mierze do deweloperów i skupia się głównie na testach jednostkowych lub pisanych przez deweloperów testach kompleksowych. Inne książki, które albo przeczytałem, albo o których słyszałem, poświęcone są konkretnej technologii automatyzacji testów, konkretnej metodyce, lub po prostu są już zbyt nieaktualne. Choć ogólnie zgadzam się z tym, że idea, zgodnie z którą to deweloperzy piszą testy, może być w wielu sytuacjach bardzo efektywna, to w rzeczywistości nie pasuje ona do wszystkich organizacji na wszystkich etapach. Co więcej, automatyzacja testów jest narzędziem, które służy i ma wpływ niemal na wszystkich interesariuszy organizacji tworzącej oprogramowanie, wliczając w to testerów, menedżerów produktu, architektów oprogramowania, ludzi z zespołów DevOps oraz menedżerów projektów, a nie tylko deweloperów. Ponieważ każda organizacja i każdy projekt oprogramowania jest inny, próba dostosowania technik, praktyk i narzędzi, które nie pasują do potrzeb lub umiejętności danego zespołu, może doprowadzić do niepowodzenia projektu automatyzacji testów, a w niektórych przypadkach nawet do upadku całego projektu oprogramowania.

Książka ta ma na celu zaprezentowanie szerokiego poglądu na temat automatyzacji testów, aby umożliwić czytelnikowi podejmowanie mądrych decyzji dotyczących jego konkretnego przypadku – biorąc przy tym pod uwagę jego ograniczenia i korzyści, jakie chce on uzyskać dzięki automatyzacji testów – ale również dostarczenie szczegółowych i praktycznych porad w zakresie efektywnej budowy automatyzacji testów, a przynajmniej dla większości przypadków.

Kto powinien przeczytać tę książkę?

Ponieważ automatyzacja testów wywiera wpływ na prawie wszystkich interesariuszy organizacji tworzącej oprogramowanie i w tej książce staramy się omówić prawie każdy aspekt automatyzacji testów, jest ona przeznaczona dla każdego, kto jest zaangażowany w proces tworzenia oprogramowania i chce dowiedzieć się, w jaki sposób można uzyskać więcej korzyści z automatyzacji testów. Do grona tych osób zaliczyć można: menedżerów zespołów zapewniania jakości, menedżerów zespołów deweloperów, deweloperów, testerów, architektów, menedżerów produktu (nazywanych również analitykami biznesowymi, analitykami systemu lub jeszcze inaczej), ludzi z zespołów DevOps itd. No i oczywiście deweloperów automatyzacji testów, których głównym zadaniem jest tworzenie testów automatycznych…

Znaczna część tej książki nie ma zbyt technicznego charakteru i skierowana jest do szerszego odbiorcy, jednak rozdziały od 11 do 14 są bardzo techniczne i skierowane do osób, które piszą kod i są dobrze zaznajomione z programowaniem obiektowym – w szczególności mam tu na myśli profesjonalnych deweloperów automatyzacji testów. Kod w tej części napisany został w języku C#, ale same koncepcje i pojęcia można z łatwością przenieść na inny obiektowy język programowania. Ponieważ języki C# i Java są do siebie podobne, programiści Java nie powinni mieć większego problemu ze zrozumieniem tego kodu. Jestem jednak przekonany, że również programiści innych języków będą w stanie łatwo go zrozumieć, a przynajmniej jego główne idee.

W szczególności mam nadzieję, że książkę tę przeczyta wielu menedżerów zespołów deweloperów i zapewniania jakości, ponieważ zwykle to oni mają największy wpływ na kształtowanie metodyki i procesów pracy w swojej organizacji, z którymi to automatyzacja testów powinna się integrować i wspomagać ich rozwój. Ponadto książka ta zawiera wskazówki i techniki przydatne dla osób niebędących menedżerami, pozwalające im usprawniać stosowane w organizacji metodyki i procesy pracy nawet bez żadnej formalnej władzy.

Jak zorganizowana jest ta książka?

Gdy po raz pierwszy usiadłem do pisania tej książki, starałem się myśleć o jej ogólnej strukturze, ale zorientowałem się, że będzie to bardzo trudne zadanie, ponieważ wygląda na to, że prawie każdy temat jest powiązany z wieloma innymi tematami. W tamtym czasie nie mogłem znaleźć przejrzystego i logicznego sposobu podzielenia jej treści na ogólne części, tak więc napisałem „gruntowny spis” tematów, które chciałem w tej książce omówić i po prostu zacząłem je pisać, przelewając swoją wiedzę bezpośrednio na papier (a mówiąc bardziej precyzyjnie, na klawiaturę…). Naturalnie rozpocząłem od najprostszych i najbardziej ogólnych rzeczy, a następnie stopniowo rozbudowywałem je o kolejne, bardziej zaawansowane i szczegółowe rozdziały. Ponieważ tematy te są ze sobą ściśle powiązane, często pisałem fragmenty odwołujące się do tematu, którego jeszcze nie napisałem, a przy bardziej zaawansowanych tematach odwoływałem się do wcześniejszych rozdziałów. Tak więc ostatecznie, niczym w dobrym projekcie Agile (a skoro już mowa o odwołaniach do innych rozdziałów, to zobacz rozdział 1, zawierający więcej informacji na temat metodyki Agile), ogólna struktura tej książki zaczęła się stopniowo ujawniać. W pewnym momencie zdałem sobie sprawę, że książka przybrała dosyć logiczną strukturę złożoną z dwóch części: pierwsza część odpowiada bardziej na ogólne pytania typu „dlaczego” oraz „co”, zaś druga część odpowiada na bardziej szczegółowe i techniczne pytania typu „jak”.

Ogólnie zachęcam czytelników do przeczytania całej książki od początku do końca. Ponieważ jednak książka ta skierowana jest do szerokiego grona odbiorców o różnych problemach, umiejętnościach, zainteresowaniach, potrzebach itd., można również skupić się na lekturze wyłącznie konkretnych rozdziałów, przeglądając lub nawet pomijając pozostałe. Można przy tym też skakać w przód i w tył do innych rozdziałów wspominanych w aktualnie czytanym fragmencie, aby w razie potrzeby uzupełnić swoją wiedzę. Wreszcie warto zawsze trzymać tę książkę w pobliżu, aby skorzystać z niej później, gdy zastosowanie automatyzacji testów w organizacji wystarczająco dojrzeje i zacznie stawiać czoło nowym wyzwaniom.

Oto przegląd poszczególnych części i rozdziałów tej książki:

Część I: „Dlaczego” oraz „Co”

Ta część omawia temat automatyzacji testów pod kątem wielu różnych aspektów, ale w bardziej „ogólny” sposób. Ta część książki jest niezbędna dla tych, którzy nie mają dużego doświadczenia z automatyzacją testów i chcą się dowiedzieć, jak wpasowuje się ona w szeroki obraz tworzenia oprogramowania, oraz od czego można zacząć. Zawarte w niej rozdziały pomogą nam również zrozumieć to, czego możemy, a także czego nie powinniśmy oczekiwać od automatyzacji testów. Jest to szczególnie istotne dla menedżerów zespołów deweloperów i zapewniania jakości, ponieważ omawiają one takie aspekty jak struktura biznesu, procesy pracy, architektura itd. Ta część książki pomoże nam przy podejmowaniu wielu decyzji, jakie nas czekają (czego wiele osób nie bierze nawet pod uwagę!) i pokaże nam, jaki wpływ może mieć każda z nich. Nawet jeśli nie jesteśmy menedżerami i uważamy, że nie mamy żadnego wpływu na te rzeczy, powinniśmy przeczytać rozdziały z tej części, aby zrozumieć ograniczenia i zalety w naszej obecnej sytuacji, a także być w stanie lepiej komunikować je naszym menedżerom.

Jeśli mamy już doświadczenie z automatyzacją testów, to ta pierwsza część pomoże nam poszerzyć w tym temacie nasze horyzonty i pokaże nam opcje oraz konsekwencje związane z decyzjami, które wcześniej podjęliśmy w mniej świadomy sposób.

Część II: „Jak”

Po ogólnym zapoznaniu się z dziedziną automatyzacji testów, czas zakasać rękawy i zacząć pisać testy wraz z wymaganą infrastrukturą. Po napisaniu kilku testów wyjaśniamy, w jaki sposób możemy zrobić krok na przód i najbardziej efektywnie wykorzystać automatyzację testów w cyklu tworzenia oprogramowania.

Od strony merytorycznej rozdziały w tej części można podzielić na dwie grupy (przy czym podział ten nie jest nigdzie jawnie podany, z wyjątkiem tego miejsca): rozdziały od 9 do 14 pisane są jako praktyczny samouczek, w ramach którego projektujemy i tworzymy system automatyzacji testów wraz z kilkoma testami (z użyciem narzędzia Selenium) dla istniejącego projektu open source, zaś rozdziały od 15 do 19 stanowią przewodnik po wykorzystywaniu automatyzacji testów w najbardziej efektywny sposób, pokazując przy tym, jak wyciągnąć z niej maksimum korzyści.

Większość rozdziałów z tej pierwszej grupy ma bardzo techniczny charakter, w przeciwieństwie do rozdziałów drugiej grupy. Z tego powodu pierwsza grupa rozdziałów jest bardziej odpowiednia dla deweloperów, a w szczególności dla deweloperów automatyzacji testów posiadających umiejętności w zakresie programowania obiektowego, natomiast druga grupa rozdziałów może być użyteczna dla każdego. Doświadczonych deweloperów zachęcam do podążania za samouczkiem krok po kroku i wykonywania wszystkich kroków samodzielnie, aby mogli oni doświadczyć ich w lepszym stopniu. Osoby, które nie potrafią programować, powinny przejrzeć te bardziej techniczne rozdziały w celu zapoznania się z głównymi koncepcjami, które są w nich zawarte, nawet jeśli osoby te nie zamierzają implementować ich w swoim własnym projekcie.

Oto kompletny opis rozdziałów:

Część I:

Rozdział 1: Wartość automatyzacji testów – w tym rozdziale wyjaśniono, dlaczego automatyzacja testów jest potrzebna i jakie są jej krótko- i długoterminowe korzyści.

Rozdział 2: Od testowania ręcznego do automatycznego – ten rozdział zawiera omówienie różnic pomiędzy testowaniem ręcznym i automatycznym oraz początek nakreślenia realistycznych oczekiwań dotyczących automatyzacji testów, ponieważ znacząco różni się ona od zwyczajnie szybszych testów manualnych.

Rozdział 3: Ludzie i narzędzia – w tym rozdziale wyjaśniono, kto powinien pisać testy i infrastrukturę automatyzacji, oraz jakie są konsekwencje stosowania alternatywnych rozwiązań. Dodatkowo omówiono sposób dobierania właściwych narzędzi w zależności od wybranej opcji.

Rozdział 4: Osiąganie pełnego pokrycia – w tym rozdziale nakreślono realistyczne oczekiwania dla długoterminowej mapy drogowej projektu automatyzacji, a także pokazano, w jaki sposób możemy zacząć czerpać z niej korzyści jeszcze na długo przed tym, jak automatyzacja zastąpi większość manualnych testów regresji.

Rozdział 5: Procesy biznesowe – w tym rozdziale wyjaśniono, w jaki sposób automatyzacja testów powiązana jest z procesami biznesowymi wytwarzania oprogramowania i podano ogólny zarys tematów, które omawiane są bardziej szczegółowo pod koniec tej książki.

Rozdział 6: Automatyzacja i architektura testów – w tym rozdziale omówiono sposób, w jaki automatyzacja testów jest powiązana z architekturą testowanego systemu, oraz dlaczego ważne jest, aby były one do siebie dostosowywane.

Rozdział 7: Izolacja i środowiska testowe – w tym rozdziale wyjaśniono, w jaki sposób należy planować automatyzację testów oraz jej środowiska wykonywania, aby zagwarantować, że testy są wiarygodne i nie mają na nie wpływu żadne niepożądane efekty.

Rozdział 8: Szersza perspektywa – w tym rozdziale omówiono wzajemne zależności pomiędzy wszystkimi tematami omawianymi w poprzednich rozdziałach – głównie między architekturą, strukturą biznesu, procesami biznesowymi i oczywiście automatyzacją testów. Omówiono również sposób, w jaki wszystkie te tematy odnoszą się do kultury biznesu.

Część II:

Rozdział 9: Przygotowanie do samouczka – ten rozdział zawiera opis proces wykorzystywany w ramach samouczka, który ma również zastosowanie w większości projektów automatyzacji testów. W rozdziale tym pokazano również, jak można skonfigurować własną maszynę, aby móc samodzielnie wykonywać kolejne kroki tego samouczka.

Rozdział 10: Projektowanie pierwszego przypadku testowego – w tym rozdziale uczymy się konkretnej techniki projektowania przypadków testowych w sposób najlepiej pasujący do testów automatycznych.

Rozdział 11: Kodowanie pierwszego testu – w tym rozdziale pokazano, w jaki sposób możemy rozpocząć pisanie kodu dla pierwszego testu. Zaczynamy od napisania prostego szkieletu testu, w sposób, który pozwoli nam zaprojektować i utworzyć modułową infrastrukturę do wielokrotnego użytku. Pod koniec tego rozdziału nasz test będzie się kompilował, ale nie będzie on wykonywał jeszcze żadnej pracy.

Rozdział 12: Uzupełnianie pierwszego testu – w tym rozdziale kończymy pracę, którą zaczęliśmy w rozdziale poprzednim. Pod koniec tego rozdziału będziemy mieć działający test oraz dobrze zaprojektowaną infrastrukturę, która będzie go obsługiwać.

Rozdział 13: Badanie niepowodzeń – w tym rozdziale ćwiczymy sposób badania i radzenia sobie z rzeczywistym niepowodzeniem testu, które miało miejsce w nowej kompilacji testowanego systemu, oraz tworzenia raportu, który pomoże nam zbadać dodatkowe niepowodzenia w przyszłości.

Rozdział 14: Dodawanie kolejnych testów – w tym rozdziale dodajemy jeden dodatkowy test. Ponadto omawiamy sposób dodawania coraz większej liczby testów przy jednoczesnym rozszerzaniu i usprawnianiu wspierającej ich infrastruktury, w tym obsługę testowania w wielu przeglądarkach, obsługę wielu środowisk i znacznie więcej.

Rozdział 15: Ciągła integracja – w tym rozdziale (rozpoczynającym drugą grupę rozdziałów z części II) wyjaśniono, w jaki sposób możemy integrować testy do postaci kompilacji ciągłej integracji. Poza aspektami technicznymi, w rozdziale tym pokazano, jak zapewnić sukces ciągłej integracji jako narzędzia organizacyjnego i podano porady dla osób bez doświadczenia w programowaniu, jak stopniowo zmieniać na lepsze kulturę i procesy danej organizacji poprzez wykorzystywanie zalet ciągłej integracji.

Rozdział 16: Tworzenie oprogramowania sterowane testami akceptacyjnymi (ATDD) – w tym rozdziale wyjaśniono korzyści ze stosowania oraz sposób implementacji metodyki tworzenia oprogramowania sterowanego testami akceptacyjnymi, która dzięki wykorzystaniu ciągłej integracji obejmuje cały cykl tworzenia oprogramowania i pomaga zespołowi efektywnie wykorzystywać metodykę Agile.

Rozdział 17: Testy jednostkowe i tworzenie oprogramowania sterowane testami (TDD) – w tym rozdziale omówiono techniki, które tradycyjnie przypisywane są wyłącznie programistom aplikacji: testy jednostkowe i tworzenie oprogramowania sterowane testami, ale są tak naprawdę nieodłączną częścią automatyzacji testów.

Rozdział 18: Inne rodzaje testów automatycznych – w tym rozdziale omówiono dodatkowe rodzaje testów automatycznych, w tym testowanie wydajności i obciążenia, testowanie w środowisku produkcyjnym, testowanie wizualne, testy instalacji, testowanie z wykorzystaniem sztucznej inteligencji i więcej.

Rozdział 19: Co dalej? – w tym rozdziale podano pewne wskazówek dotyczące dalszego zdobywania i rozwijania umiejętności w zakresie automatyzacji testów.

Poza tymi rozdziałami, na końcu książki dostępne są również cztery dodatki:

Dodatek A: Rzeczywiste przykłady – ten dodatek stanowi uzupełnienie rozdziału 6 („Automatyzacja i architektura testów”) i zawiera cztery rzeczywiste przykłady architektur aplikacji oraz odpowiadające im rozwiązania automatyzacji.

Dodatek B: Mechanizm oczyszczania – ten dodatek zawiera opis sposób budowy mechanizmu oczyszczania, przedstawionego w rozdziale 7 („Izolacja i środowiska testowe”).

Dodatek C: Projekt „Test Automation Essentials” – w tym dodatku opisałem stworzony przeze mnie projekt open source o nazwie Test Automation Essentials, zawierający wiele przydatnych narzędzi kodu (napisanych w C#) dla projektów automatyzacji testów.

Dodatek D: Wskazówki i praktyki zwiększające produktywność programisty – ten dodatek stanowi uzupełnienie dla rozdziałów od 9 do 14 i zawiera wskazówki, które pozwolą nam zwiększyć produktywność podczas programowania. Choć wskazówki te mogą być przydatne dla dowolnego programisty, będą one szczególnie użyteczne dla programistów automatyzacji testów.

Przyjemnej lektury!

Część I. „Dlaczego” oraz „co”

Ponieważ książka ta nosi tytuł Automatyzacja testów. Kompletny przewodnik dla testerów oprogramowania, obejmuje ona teorię i praktykę, tematy na poziomie początkowym i zaawansowanym, metodologiczne aspekty i techniczne szczegóły itd. Jest tak, ponieważ próbowałem się odnieść w tej jednej książce do tak wielu różnych pytań dotyczących automatyzacji testów, jak to tylko możliwe.

W pierwszej części tej książki staramy się udzielić odpowiedzi głównie na pytania typu „dlaczego” oraz „co”, pozostawiając większość pytań typu „jak” na drugą część. Rozpoczynamy od wyjaśnienia, dlaczego w ogóle potrzebujemy automatyzacji testów i na czym tak naprawdę ona polega (jak również na czym nie polega). Następnie odpowiadamy na wiele pytań, rozwiązujemy wiele dylematów i omawiamy wiele czynników (tj. której opcji powinniśmy użyć i dlaczego) dotyczących automatyzacji testów, które będą istotne dla każdego, kto planuje rozpocząć korzystanie z automatyzacji testów lub usprawnić istniejącą automatyzację. Na koniec spoglądamy na to wszystko z dalszej perspektywy i patrzymy, jak te wszystkie elementy są ze sobą powiązane.

Przyjemnej lektury!

Rozdział 1. Wartość automatyzacji testów

Ponieważ tematem tej książki jest automatyzacja testów, powinniśmy w zasadzie zacząć od jej definicji. Jednak bez nakreślenia właściwego kontekstu definicja taka może nie być wystarczająco przejrzysta i może bardziej prowadzić do dezorientacji niż zrozumienia. Jest to na tyle szeroki i zróżnicowany temat, że trudno jest tu podać taką definicję, która będzie jednocześnie dokładna, przejrzysta i obejmie wszystkie istniejące rodzaje automatyzacji testów. Gdybym jednak miał teraz przytoczyć jakąś definicję, mogłaby ona wyglądać tak: „Używanie oprogramowania w celu ułatwienia testowania innego oprogramowania”, ale nie jestem do końca pewien, na ile jest ona przydatna. Dlatego też zamiast skupiać się na formalnych definicjach, w pierwszej części książki szczegółowo analizuję ten obszerny temat, starając się w ten sposób wyjaśnić, czym tak naprawdę jest automatyzacja testów, a także – co równie istotne – czym ona nie jest!

Dlaczego potrzebujemy automatyzacji testów?

Gdy pytam moich klientów, czego spodziewają się uzyskać dzięki automatyzacji testów, najczęściej udzielaną odpowiedzią jest skrócenie czasu potrzebnego na przetestowanie oprogramowania przed jego wydaniem. Z jednej strony, choć jest to niewątpliwie ważny cel, to w zakresie korzyści, jakie możemy uzyskać dzięki automatyzacji testów, stanowi on jedynie wierzchołek góry lodowej. Ponadto osiągnięcie celu w postaci skrócenia cykli testów manualnych zajmuje zwykle sporą ilość czasu. Z drugiej strony, dużo wcześniej możemy zacząć zauważać pozostałe korzyści. Ale najpierw zobaczmy, dlaczego ten prosty cel w postaci skrócenia czasu trwania cyklu testowego stał się w ostatnich latach tak istotny.

Od modelu kaskadowego do zwinnego tworzenia oprogramowania

Mimo że niektóre organizacje korzystają z automatyzacji testów już od dekad, jednak zaczęła być ona powszechnie stosowana dopiero w ostatnich latach. Jest wiele powodów, dla których tak się stało, ale bez wątpienia można powiedzieć, że wzrost zapotrzebowania na automatyzację testów zawdzięczamy w dużej mierze odejściu od tradycyjnego modelu kaskadowego (waterfall) na rzecz programowania zwinnego (Agile software development). W tradycyjnym podejściu kaskadowym projekty oprogramowania postrzegane były jako coś jednorazowego, podobnie jak budowanie mostu. Najpierw planujemy i projektujemy oprogramowanie, potem je budujemy, a na końcu testujemy i sprawdzamy jakość końcowego produktu, naprawiając przy tym pomniejsze błędy, które znaleźliśmy. Opieramy się tu na założeniu, że jeśli fazy planowania i budowy zostały przeprowadzone poprawnie, to poza pewnymi drobnym pomyłkami programistycznymi, które możemy bardzo łatwo naprawić, wszystko powinno działać zgodnie z planem. Takie podejście sprawia, że proces weryfikowania, czy rezultat końcowy zachowuje się zgodnie ze specyfikacją, musimy przeprowadzić tylko raz. Ponowne wykonanie testu powinno mieć miejsce tylko w przypadku wykrycia jakiegoś błędu i przygotowania dla niego odpowiedniej poprawki, a następnie jej sprawdzenia. Jeśli każdy test wykonywany jest tylko raz lub dwa razy, to w wielu przypadkach znacznie taniej i łatwiej będzie wykonywać je ręcznie niż je automatyzować.

Po latach stało się jasne, że w większości przypadków podejście kaskadowe nie spełnia swoich obietnic. Większość projektów oprogramowania była już na tyle skomplikowana, że zaplanowanie i domknięcie wszystkich technicznych szczegółów w początkowej fazie tworzenia było niemożliwe. Nawet w tych przypadkach, w których było to wykonalne, do czasu ukończenia takiego projektu (trwającego zwykle kilka lat) zmieniały się zarówno sama technologia, jak i potrzeby biznesowe, czyniąc takie oprogramowanie mniej adekwatnym niż miało być początkowo. Z tych właśnie powodów szybkie reagowanie na opinie klientów stało się cenniejsze od sztywnego trzymania się początkowego planu. Wraz z upływem czasu większość przemysłu oprogramowania odeszła od tych jednorazowych projektów, rezygnując z cyklicznego wydawania nowych wersji tego samego oprogramowania co kilka lat na rzecz szybkich cyklów wydawniczych. Dzisiaj niektóre z największych firm działających w sieci Web dostarczają nowe funkcje i poprawki dla swojego oprogramowania po kilka razy dziennie, a niekiedy nawet kilka razy na minutę!

MANIFEST PROGRAMOWANIA ZWINNEGO

W 2001 roku 17 liderów z obszaru rozwoju oprogramowania sformułowało Manifest programowania zwinnego, którego treść jest następująca¹:

Odkrywamy nowe metody programowania dzięki praktyce w programowaniu i wspieraniu w nim innych. W wyniku naszej pracy zaczęliśmy bardziej cenić:

Ludzi i interakcje od procesów i narzędzi

Działające oprogramowanie od szczegółowej dokumentacji

Współpracę z klientem od negocjacji umów

Reagowanie na zmiany od realizacji założonego planu

Oznacza to, że elementy wypisane po prawej są wartościowe, ale większą wartość ma ją dla nas te, które wypisano po lewej.

Kent Beck

James Grenning

Robert C. Martin

Mike Beedle

Jim Highsmith

Steve Mellor

Arie van Bennekum

Andrew Hunt

Ken Schwaber

Alistair Cockburn

Ron Jeffries

Jeff Sutherland

Ward Cunningham

Jon Kern

Dave Thomas

Martin Fowler

Brian Marick

© 2001, autorzy powyżej

Deklaracja ta może być swobodnie kopiowana w dowolnej formie, ale wyłącznie w całości, z uwzględnieniem tej uwagi.

Oczywiście nie wszystkie firmy i zespoły przyjmują te zasady, ale prawie każdy, kto jest dziś zaangażowany w rozwój oprogramowania, preferuje szybsze dostarczanie nowych wersji (i dalsze ich dostarczanie na przestrzeni długiego okresu czasu), zamiast dostarczać tylko kilka nowych wersji z długimi przerwami między nimi. Oznacza to również, że zmiany pomiędzy kolejnymi wydaniami są teraz mniejsze niż w przypadku dostarczania nowej wersji raz na kilka lat. Naturalnie firmy tworzące oprogramowanie dla sektorów o kluczowym znaczeniu są mniej skłonne do podejmowania ryzyka, dlatego zwykle wydają one oprogramowanie w stosunkowo długich cyklach wydawniczych. Jednak już nawet i one zaczynają dostrzegać korzyści w szybszym dostarczaniu oprogramowania, przynajmniej wewnętrznie do zespołów QA.

Manualne testowanie każdej takiej wersji programu może zająć mnóstwo czasu, co stanowi oczywisty powód, dla którego automatyzacja testów stała się tak istotna. Nie jest to jednak jedyny ważny powód.

Koszt złożoności oprogramowania

Do każdej nowej wersji programu dodawane są nowe funkcje. W miarę dodawania funkcji oprogramowanie staję się coraz bardziej złożone, a wówczas coraz trudniej dodawać do niego nowe funkcje, nie psując przy tym istniejącego kodu. Jest to szczególnie widoczne przy dużej presji na szybkie dostarczanie nowych wersji oprogramowania, gdy nie poświęca się wystarczająco dużo czasu na planowanie i poprawę jakości kodu (jak to często ma miejsce w przypadku źle wdrożonej metodyki Scrum²). Ostatecznie następuje spadek szybkości dostarczania nowych wersji, czyli coś, czego chcemy uniknąć od samego początku!

Części z dodanej w ten sposób złożoności nie da się uniknąć. Będzie ona istnieć nawet wtedy, gdy skrupulatnie zaplanujemy i zaprojektujemy z góry całe oprogramowanie. Jest to tzw. złożoność wewnętrzna (inherent complexity). Jednak w większości przypadków powodami istnienia przeważającej części złożoności oprogramowania jest szybkie wprowadzanie nowych funkcji bez odpowiedniego projektu, brak komunikacji wewnątrz zespołu lub brak odpowiedniej wiedzy – czy to w zakresie wykorzystywanej technologii, czy też w obszarze potrzeb biznesowych. Teoretycznie złożoność tę można by zredukować poprzez skrzętne zaplanowanie z góry całego oprogramowania, ale w rzeczywistości jest ona naturalną częścią każdego projektu oprogramowania. Ten typ złożoności nazywany jest często złożonością przypadkową (accidental complexity).

Każda złożoność – czy to wewnętrzna, czy przypadkowa – niesie ze sobą pewien koszt. Koszt ten jest oczywiście częścią całkowitego kosztu wytworzenia oprogramowania, na który wpływ ma głównie liczba programistów i testerów pomnożona przez czas potrzebny im na dostarczenie oprogramowania (oczywiście pomnożona również przez ich wynagrodzenie). Zgodnie z powyższym, gdy złożoność fragmentu oprogramowania wzrasta, jego koszt również rośnie, ponieważ przetestowanie wszystkiego oraz naprawa znalezionych błędów (i ponowne przetestowanie kodu) wymaga większej ilości czasu. Ponadto złożoność przypadkowa sprawia również, że oprogramowanie jest bardziej ułomne i trudniejsze w utrzymaniu, a co za tym idzie, wymaga jeszcze więcej czasu na przetestowanie i naprawę błędów.

Utrzymywanie stałego kosztu

Na rysunku 1.1 pokazano to, co chcemy osiągnąć: koszt utrzymywany na stałym poziomie w miarę dodawania nowych funkcji. Niestety dodawanie nowych funkcji oznacza zwykle zwiększanie złożoności oprogramowania, co naturalnie zwiększa jego koszt. Istnieją jednak dwa czynniki, które mogą nam pomóc w utrzymaniu kosztu na stałym poziomie:

1. Minimalizowanie kosztów uruchamiania stale powiększającego się zestawu testów regresji.

2. Pisanie kodu, który jest łatwy w utrzymaniu.

Pierwszy czynnik możemy łatwo osiągnąć poprzez zautomatyzowanie większości testów. Natomiast na drugi czynnik duży wpływ ma złożoność przypadkowa, przez co znacznie trudniej go kontrolować.

W kodzie, który jest łatwy w utrzymaniu, złożoność wprowadzana do oprogramowania w wyniku dodawania do niego nowych funkcji ma bardzo mały lub zerowy wpływ na złożoność funkcji już istniejących. Oznacza to, że jeśli wzrost złożoności będziemy utrzymywać w tempie liniowym, to nadal będziemy mogli utrzymać stabilny koszt, jak to pokazano na rysunku 1.2. Oczywiście tę zdolność do dodawania złożoności chcielibyśmy zachować jedynie dla złożoności wewnętrznej (tj. nowych funkcji) i nie marnować jej na złożoność przypadkową. Niestety złożoność przypadkowa sprawia, że w większości rzeczywistych projektów, w miarę dodawania kolejnych funkcji złożoność rośnie dużo szybciej niż liniowo (patrz rysunek 1.3). To z kolei powoduje również wzrost kosztów dodawania nowych funkcji na przestrzeni czasu, co zostało przedstawione na rysunku 1.4.

Rysunek 1.1. Pożądany koszt dodawania nowych funkcji na przestrzeni czasu

Rysunek 1.2. Pożądany wzrost złożoności po dodaniu nowych funkcji jest liniowy

Rysunek 1.3. Powszechny przypadek: złożoność rośnie dużo szybciej w powodu dodanej złożoności przypadkowej

Rysunek 1.4. Koszt rozwoju oprogramowania w typowym przypadku: dodawanie nowych funkcji staje się coraz bardziej kosztowne wraz z upływem czasu

W większości przypadków przerywanie dotychczasowych prac i planowanie wszystkiego od początku w celu obniżenia złożoności przypadkowej jest całkowicie niepraktyczne. A nawet gdyby byłoby inaczej, to do czasu zrównania się pod względem funkcjonalnym nowej (tworzonej od zera) wersji z wersją poprzednią, będzie już ona miała swoją własną złożoność przypadkową…

Refaktoryzacja

Wydaje się więc, że tworzenie kolejnych nowych funkcji na ustabilizowanym poziomie kosztów jest niemożliwe, ponieważ złożoność przypadkowa jest nieunikniona. Czy jesteśmy zatem skazani na porażkę? Cóż… nie do końca. Rozwiązaniem pozwalającym trzymać złożoność przypadkową pod kontrolą jest refaktoryzacja. Refaktoryzacja jest procesem polegającym na usprawnianiu projektu (lub „wewnętrznej struktury”) danego fragmentu oprogramowania, bez wpływu na jego zewnętrzne zachowanie. Innymi słowy, refaktoryzacja pozwala nam pozbyć się złożoności przypadkowej. Refaktoryzacji możemy dokonywać małymi krokami, usprawniając nasz projekt kawałek po kawałku, bez konieczności przeprojektowywania całego systemu. W książce Martina Fowlera, Refaktoryzacja. Ulepszanie struktury istniejącego kodu³, podano odpowiednie techniki pozwalające dokonywać refaktoryzacji w bezpieczny sposób. Obecnie najpopularniejsze zintegrowane środowiska programistyczne⁴ zawierają pewne narzędzia do automatycznej refaktoryzacji lub oferują dodatki, które je dostarczają.

Ale nawet w przypadku używania narzędzi do automatycznej refaktoryzacji może dojść do pomyłki programisty, w wyniku której w projekcie pojawią się błędy psujące jakąś istniejącą funkcjonalność. Z tego powodu refaktoryzacja wymaga również przeprowadzania wyczerpujących testów regresji. Tak więc w celu utrzymania szybkiego tempa wydawania nowych i stabilnych wersji zawierających nowe funkcje, musimy regularnie refaktoryzować nasz kod. Aby być w stanie to robić, musimy bardzo często go testować. Jest to drugi ważny powód, dla którego powinniśmy stosować automatyzację testów. Rysunek 1.5 pokazuje, w jaki sposób refaktoryzacja pomaga trzymać złożoność przypadkową pod kontrolą.

Rysunek 1.5. Refaktoryzacja pomaga trzymać złożoność pod kontrolą

Ciągłe doskonalenie

To, co fascynuje mnie najbardziej w automatyzacji testów, to jej związki ze wszystkimi innymi aspektami cyklu tworzenia oprogramowania. Poza związkiem z jakością i produktywnością, który jest oczywisty, automatyzacja testów powiązana jest również z architekturą tworzonego produktu, procesami biznesowymi, strukturą organizacyjną, a nawet z kulturą biznesu (patrz rysunek 1.6). Dla mnie osobiście automatyzacja testów jest niczym lustro, które odzwierciedla wszystkie te rzeczy. Każdy z tych aspektów ma pewien wpływ na automatyzację testów. Ale odzwierciedlenie tych wpływów w automatyzacji testów możemy również wykorzystać do zmiany i usprawnienia dowolnego z tych aspektów.

W wielu przypadkach klienci, którzy korzystają już z automatyzacji testów, proszą mnie o pomoc w rozwiązaniu napotkanych problemów. Problemy te bardzo często objawiają się na poziomie technicznym. Kiedy jednak przychodzę do tych klientów i pomagam im rozpoznać bezpośrednią przyczynę tych problemów, często okazuje się, że są one tak naprawdę związane z co najmniej jednym z tych pozostałych aspektów. Pozbycie się tych problemów nie zawsze jest proste, ale przynajmniej uświadamiają sobie znaczenie tych problemów, co jest pierwszym krokiem prowadzącym do zmiany.

Rysunek 1.6. Automatyzacja testów powiązana jest z wieloma innymi aspektami tworzenia oprogramowania

Mam nadzieję, że po przeczytaniu tej książki będziemy mogli lepiej dostrzegać wpływy, jakie tego rodzaju problemy wywierają na budowaną przez nas automatyzację testów i będziemy w stanie uświadomić ich istnienie odpowiednim osobom, umożliwiając w ten sposób dokonanie niezbędnych usprawnień. Oczywiście, jeśli w zespole panuje kultura ciągłego doskonalenia (np. organizowanie retrospektywnych spotkań i opieranie na nich swoich działań), wówczas będzie to łatwiejsze do zrobienia. A nawet jeśli tak nie jest, pamiętajmy, że świadomość jest kluczem pozwalającym dokonać zmiany i że automatyzacja testów pomoże to osiągnąć, nawet na stanowisku młodszego programisty ds. automatyzacji w dużej i biurokratycznej firmie (więcej informacji na temat tego, w jaki sposób stopniowo zmieniać strukturę swojej organizacji w celu skorzystania z zalet automatyzacji testów, można znaleźć w rozdziale 17).

Rozdział 2. Od testowania ręcznego do automatycznego

Spójrzmy prawdzie w oczy: żyjemy w XXI wieku. Nie istnieje żaden powód, dla którego dowolne z powtarzalnych zadań nie może zostać poddane pełnej automatyzacji, zwłaszcza w środowiskach o wysokim stopniu zaawansowania technicznego. Wciąż jednak spora część pracy testera manualnego polega na wykonywaniu testów regresji⁵, co jest bardzo monotonne. Oczywiście wykonywanie ich w sposób ręczny jest znacznie wolniejsze i bardziej podatne na błędy w porównaniu z tym, co może potencjalnie robić komputer.

Podejście pierwsze: nagrywanie i odtwarzanie

Pierwszą myślą każdego, kto chciałby usprawnić ten proces, jest automatyzacja pracy testera manualnego. Jak dowiemy się w dalszej części tej książki, możemy to osiągnąć na kilka różnych sposobów, ale najprostszym z nich jest zwykłe nagranie czynności wykonywanych przez testera manualnego i ich późniejsze wielokrotne odtwarzanie. Zwykle polega to na nagrywaniu interakcji użytkownika z interfejsem użytkownika, ale może to być również rejestrowanie ruchu sieciowego, takiego jak żądania HTTP, lub innego rodzaju danych, które stanowią pośrednie odzwierciedlenie wykonywanych przez użytkownika czynności.

Gdyby to było takie proste, książka ta nie byłaby nam w ogóle potrzebna (a ja prawdopodobnie musiałby znaleźć sobie inną pracę…). W praktyce jednak jest to dużo bardziej skomplikowane. Mimo że duża część wykonywanych testów regresji jest wysoce powtarzalna, istnieje co najmniej jedna niepowtarzająca się część, która stanowi całą istotę wykonywania testów. Tą niepowtarzalną częścią jest część związana z wykrywaniem błędów! O ile nagrywanie wykonywanych przez testera czynności i ich późniejsze odtwarzanie jest stosunkowo proste, to już wykrywanie błędów w zautomatyzowany sposób jest znacznie trudniejsze.

Pewnym, choć dosyć naiwnym podejściem do automatycznego wykrywania błędów, jest porównywanie obrazu widocznego na ekranie z obrazem oczekiwanym, który został nagrany. Podejście to ma jednak kilka wad, z których część ma jedynie charakter techniczny. Przykładowo, mogą wystąpić pewne różnice na poziomie rozdzielczości ekranu, różnice w zakresie wyświetlanej na ekranie daty i godziny, różnice w dowolnych danych wyświetlanych na ekranie, które nie są istotne dla testu itd. Niektóre narzędzia pozwalają nam przezwyciężyć te techniczne problemy poprzez wykluczenie tych obszarów zrzutu ekranu, w których mogą pojawiać się uzasadnione różnice. Podobny problem istnieje w przypadku rejestrowania ruchu sieciowego HTTP, ponieważ część danych może zawierać uzasadnione różnice. Tutaj również możemy skorzystać z narzędzi, które pozwolą nam zdecydować o tym, które fragmenty odpowiedzi chcemy porównywać, a które wykluczyć. A nawet jeśli te techniczne problemy uda nam się rozwiązać przy użyciu narzędzi, to nadal istnieć będzie duża wada będąca nieodłączną częścią koncepcji nagrywania i odtwarzania: każda dozwolona zmiana w aplikacji będzie uznawana za defekt, co znacząco utrudni nam rozróżnianie wyników fałszywie dodatnimi od faktycznych niepowodzeń.

Na tym etapie można by powiedzieć: „Wielka rzecz! Wykonujemy testy regresji, aby upewnić się, że wszystko działa dokładnie tak jak do tej pory!”. Pozwólcie, że odpowiem w ten sposób: jeśli nikt nie ruszał kodu⁶ testowanego systemu (system under test, SUT), to nie wystąpi żadna regresja, a tym samym nie ma sensu marnować czasu na wykonywanie jakichkolwiek testów – czy to ręcznie, czy automatycznie. Z drugiej strony, żaden programista nie będzie raczej zmieniał kodu, jeśli nie zamierza on wprowadzać zmiany w zachowaniu aplikacji! Stąd też wykonywanie testów ma sens jedynie w przypadku pojawienia się zmian, a zatem przy każdym wykonywaniu testów powinniśmy oczekiwać, że coś zostało zmienione.

Gdy wykonujemy testy ręcznie, rzadko uznajemy te zmiany za problem. Często zmiany te są na tyle małe, że jeśli tylko będziemy kierować się naszym własnym osądem i zdrowym rozsądkiem, to nadal słowa opisujące kolejne kroki testu będziemy mogli przełożyć na nowe zachowanie aplikacji, nawet jeśli nie pasują już do siebie dokładnie. Ten osąd i zdrowy rozsądek – oparte na naszej wiedzy, doświadczeniu, komunikacji z innymi członkami zespołu itd. – wykorzystujemy do oceny, czy jakaś zmiana jest błędem, czy usprawnieniem. Niestety maszyna nie dysponuje takimi umiejętnościami, więc wszystkie błędy i uprawnione zmiany traktuje na równi.

Jeśli spodziewane wyniki naszych testów są zbyt ogólne (czyli „dokładnie tak, jak było”), zamiast odzwierciedlać wyłącznie szczegółowe informacje, których przetestowanie jest istotne, to nasze testy zbyt często będą kończyć się niepomyślnie z powodu uprawnionych zmian, a nie prawdziwych błędów. Innymi słowy, stosunek wyników fałszywie dodatnich do prawdziwych niepowodzeń będzie zbyt wysoki, przez co nasze testy będą mniej wiarygodne! Bez przejrzystych i zwięzłych spodziewanych wyników prawdopodobnie natkniemy się na poniższe problemy:

1. Każda uprawniona zmiana w aplikacji będzie powodować te same błędy przy każdym kolejnym uruchomieniu testu, dopóki nie nagramy ponownie testu lub go nie naprawimy.

2. Jeśli dany test będziemy w kółko nagrywać ponownie, istnieje duża szansa, że w nagrywanym przez nas scenariuszu pojawią się błędy. Oczywiście może się zdarzyć, że błąd pojawi się już przy pierwszym nagrywaniu, a przy kolejnym zostanie już naprawiony, ale inne techniki (omawiane w dalszej części tego rozdziału) są lepiej dostosowane do stopniowego usprawniania i umacniania testów. Bez możliwości umacniania i stabilizowania testów ludzie zaczną tracić zaufanie do projektu automatyzacji jako całości.

3. Często mała zmiana w aplikacji wpływa na wiele scenariuszy testowych. Nawet jeśli wpływ tej zmiany jest bardzo mały dla człowieka, powoduje on, że wiele testów automatycznych kończy się niepowodzeniem. Na przykład poprawienie literówki w tekście przycisku czy nawet usunięcie zdublowanej spacji może sprawić, że wiele testów będzie kończyć się niepomyślnie, jeśli w ramach swojego scenariusza będą one wyszukiwać przycisku po jego tekście i go klikać.

4. Badanie wyniku wyłącznie na podstawie różnicy pomiędzy wynikiem faktycznym a oczekiwanym (czy to w formie zrzutu ekranu, czy też innego rodzaju danych, które można porównać z faktycznym rezultatem), może nie dać nam wystarczającej ilości informacji potrzebnych do ustalenia, czy mamy do czynienia z błędem, czy uprawnioną zmianą. W przypadku gdy będzie to błąd, nie będziemy mieć również odpowiednich informacji koniecznych do ustalenia jego przyczyny. Więcej informacji na temat badania testów kończących się niepowodzeniem można znaleźć w rozdziale 13.

Ostatecznie wysiłek, jaki trzeba będzie podjąć w celu zbadania tych niepowodzeń i utrzymania testów (ich ponowne nagrywanie i naprawianie), prawdopodobnie przewyższy koszt ręcznego wykonywania tych testów.

Uzyskiwanie maksimum korzyści z automatyzacji testów

Spójrzmy na to z drugiej strony: zamiast patrzeć na cele automatyzacji testów z perspektywy tego, co mamy dzisiaj (testy manualne) i jak możemy to zautomatyzować, spójrzmy na pożądany, idealny rezultat tego, co możemy w najlepszym wypadku osiągnąć za jej pomocą.

Zanim to jednak zrobimy, chciałbym najpierw wyjaśnić, że choć nakreślany tu przeze mnie obraz może być wykonalny dla kilku zespołów, to dla większości z nich nie będzie on zbyt praktyczny w tej postaci. Niemniej jednak większość zespołów powinna być w stanie wystarczająco się do niego zbliżyć i wykorzystać większość z jego zalet, przy założeniu, że zespoły zostaną właściwie pokierowane przez odpowiednią osobę (może być to ktokolwiek, również my sami, nawet jeśli nie jesteśmy menedżerami!). W każdym razie, chciałbym tu przedstawić ogólny zarys tego, co powinno być naszym celem. W pozostałej części tej książki będziemy rozmawiać o kompromisach i decyzjach, które będziemy musieli podjąć, aby zbliżyć się do celu, jaki zamierzamy zaproponować, ale bądźmy również pragmatyczni i praktyczni w kontekście tego, co prawdopodobnie osiągniemy. Pamiętajmy jednak, że jeśli podejdziemy do tego poważnie, to na przestrzeni wystarczająco długiego okresu czasu będziemy mogli lepiej przywyknąć do tych idei, co przybliży nas do celu. Więcej szczegółów i pomysłów dotyczących tego, jak stopniowo zmieniać kulturę naszej organizacji w celu lepszego wykorzystania automatyzacji testów, można znaleźć w rozdziale 15.

Tak więc teraz uspokójmy się, zamknijmy oczy i wyobraźmy sobie… no nie, musimy przecież mieć oczy otwarte, aby móc czytać dalej…

Wyobraźmy sobie, że mamy pełne pokrycie automatycznymi testami regresji, których wykonanie trwa łącznie kilka minut. Wyobraźmy sobie również, że nasz zespół naprawił wszystkie znane błędy i wszystkie testy kończą się sukcesem… Wyobraźmy sobie też, że każdy z programistów może uruchamiać dowolny z tych testów na swoim własnym komputerze deweloperskim, kiedy tylko ma na to ochotę!

Jak w takim wypadku zmieniłby się sposób wykorzystywania przez nas automatyzacji testów podczas tworzenia kolejnej wersji, funkcji czy historyjki użytkownika (zobacz tekst uzupełniający: „historyjki użytkownika”)? Czy nadal byśmy uruchamiali nasze testy wyłącznie w nocy i badali ewentualne niepowodzenia następnego ranka? Czy po natknięciu się na błąd, zgłosimy go w naszym systemie śledzenia błędów i poczekamy do końca kwartalnego cyklu wydawniczego, aż zostanie on naprawiony przez programistów? Na wszystkie te pytania odpowiedź powinna brzmieć „Nie”!

Jeśli testy wykonują się tak szybko, możemy sprawić, że będą się one uruchamiać automatycznie przed każdą operacją ewidencjonowania zmian⁷ każdego programisty (co też może być wykonywane bardzo często) i będą w stanie powstrzymać wykonanie operacji ewidencjonowania w przypadku niepomyślnego zakończenia jednego lub więcej testów. W ten sposób możemy zagwarantować, że wszystko, co znajduje się w repozytorium kontroli źródeł, zawsze będzie przechodzić wszystkie testy! Idea ta realizowana jest przez koncepcję określaną mianem ciągłej integracji (Continuous Integration, CI). Więcej informacji na ten temat można znaleźć w rozdziale 15. W rezultacie posiadanie pełnego pokrycia regresji działającego w ramach CI potencjalnie zapobiega wkradaniu się wszystkich błędów regresji! Tak więc, jeśli już na samym początku nie mieliśmy żadnych błędów, proces ten uchroni nas również przed wystąpieniem regresji, pozwalając nam potencjalnie w nieskończoność utrzymywać ten zerowy stan znanych błędów! W rzadkim przypadku znalezienia nowego błędu regresji (ręcznie, po tym jak programista zaewidencjonował kod), błąd ten może zostać najpierw odtworzony za pomocą nowego testu automatycznego i natychmiast naprawiony w celu utrzymania zerowej liczby znanych błędów.

Co więcej, zachęca to programistów do usprawniania wewnętrznej jakości kodu i struktury aplikacji, gdyż mogą oni dowolnie refaktoryzować swój kod i nadal mieć pewność, że niczego w ten sposób nie popsują (znaczenie refaktoryzacji omawiane jest w rozdziale 1). Ta wewnętrzna jakość często przekłada się również na jakość zewnętrzną, ponieważ prostszy kod ma zwykle mniej błędów i jest łatwiej go utrzymać bez wprowadzania do niego nowych błędów. Dodatkowo kod łatwiejszy w utrzymaniu, oznacza również wyższą produktywność, ponieważ pozwala on programistom szybciej, łatwiej i przede wszystkim bezpieczniej implementować coraz więcej funkcji.

A co z nowymi funkcjami? Nie będziemy tutaj zbytnio wchodzić w szczegóły (więcej na ten temat można znaleźć w rozdziale 16, poświęconym metodyce tworzenia oprogramowania sterowanego testami akceptacyjnymi, ATDD), ale podstawowa idea polega na tym, że za każdym razem, gdy opracowywana jest jakaś nowa funkcja, wraz z nią tworzone są powiązane z nią testy. Funkcja taka (lub historyjka użytkownika) uznawana jest za „ukończoną” tylko wtedy, gdy przejdzie ona pomyślnie wszystkie testy i nie zostaną w niej znalezione żadne błędy.

Już słyszę te sceptyczne głosy: „Świetnie… ale to nigdy nie zadziała w moim zespole…”. Przekonajmy się więc, że może być inaczej: jeśli zaczniemy stosować te idee już od pierwszego dnia, wówczas podejście to będzie łatwe do zrealizowania. Prawdopodobnie jednak nie zrobimy tego, tak więc w przypadku ich zastosowania w (dużo) późniejszym czasie osiągnięcie tego celu będzie dużo trudniejsze. Jednak w rozdziale 4 wyjaśniamy, w jaki sposób możemy stopniowo zbliżać się do tego celu i osiągnąć większość z omówionych wcześniej korzyści w dużo krótszym czasie. Ponadto w rozdziale 15 pokazujemy, że dowolny, nawet nasz własny zespół jest w stanie to osiągnąć. W rozdziale tym prezentujemy konkretne porady dotyczące tego, w jaki sposób przedstawić szybki wzrost z inwestycji każdemu interesariuszowi, który może sprzeciwiać się wprowadzeniu tej zmiany, nawet jeśli nie jesteśmy menedżerami.

HISTORYJKI UŻYTKOWNIKA

Historyjka użytkownika (user story) jest terminem stosowanym w ramach metodyki zwinnego tworzenia oprogramowania, który w zwięzły sposób opisuje żądaną funkcję. Zamiast definiować wyczerpujący i szczegółowy dokument wymagań na początku projektu, a następnie implementować go przez długi okres czasu, jak ma to zwykle miejsce w modelu kaskadowym, w ramach metodyki programowania zwinnego stosuje się podejście polegające na przyrostowym dodawaniu do oprogramowania małych funkcji. Każda taka niewielka funkcja lub zmiana jest historyjką użytkownika. Historyjka użytkownika nie zawsze musi się jednak wiązać z dodaniem nowej funkcji. W niektórych przypadkach historyjka użytkownika może być również żądanie zmiany lub nawet usunięcia istniejącej funkcji w wyniku uzyskania informacji zwrotnej od użytkownika.

Historyjki użytkownika powinny mieć stosunkowo wąski zakres, tak aby można je było szybko zaimplementować i dość wcześnie oddać w ręce klienta (lub przynajmniej w ręce właściciela produktu) w celu uzyskania informacji zwrotnych. Jednak, mimo że historyjka użytkownika powinna być zwięźle zdefiniowana, to nadal powinna ona stanowić jakąś wartość dla użytkownika końcowego. Zwykle potrzeba pewnej praktyki i kreatywności, aby móc rozłożyć dużą funkcję na szereg takich historyjek⁸, ale rzadko kiedy trafiają się funkcje, których nie da się w ten sposób podzielić.

Choć nic nie powstrzymuje nas przed bardzo szczegółowym opisywaniem historyjki użytkownika, to powinniśmy się raczej skupić na ogólnej idei i jej wartości dla użytkownika końcowego, pozwalając zespołowi na opracowanie własnych kreatywnych rozwiązań danego problemu.

Dosyć powszechne jest definiowanie historyjki użytkownika za pomocą poniższego szablonu lub czegoś na jego wzór:

Jako <rola>

Aby móc <cel>

Chcę <ogólny opis funkcji>

Na przykład:

Jako administrator witryny

Aby móc uniemożliwić przeprowadzenie ataku "Denial of Service"

Chcę być w stanie kontrolować maksymalną liczbę żądań na sekundę wysyłanych z adresu IP każdego klienta

Różnice pomiędzy testami manualnymi i automatycznymi

Gdy już rozumiemy, że ślepe naśladowanie pracy testera manualnego to za mało, i widzimy, że pomyślna implementacja automatyzacji testów może nam przynieść wielkie korzyści, których nie jesteśmy w stanie uzyskać w ramach ręcznego testowania regresji, możemy skonkludować, że testowanie manualne i automatyczne zasadniczo różnią się między sobą. Przyjrzyjmy się zatem bardziej szczegółowo różnicom, które między nimi występują. O różnicach tych warto szczególnie pamiętać wtedy, gdy przyjdzie nam implementować istniejące plany testów manualnych w formie testów automatycznych.

Wykonywanie testów manualnych możemy w zasadzie podzielić na dwa typy:

testowanie eksploracyjne,

testowanie zaplanowane.

Różne organizacje i zespoły mają różne zasady (rygorystyczne i wymuszane lub istniejące tylko w praktyce) dotyczące tego kto, kiedy i czy w ogóle tworzy i planuje testy. Niektóre zespoły skupiają się głównie na testach eksploracyjnych i jeszcze może na kilku scenariuszach poprawności, które są wyłącznie w głowie testującego. Jest to częściej spotykane w małych zespołach rozpoczynających działalność lub w niewielkich zespołach programistów będących częścią większej organizacji, która nie skupia się na tworzeniu oprogramowania. Po drugiej stronie tego spektrum wysoce biurokratyczne zespoły opierają się głównie na szeroko udokumentowanym, precyzyjnie zaplanowanym testowaniu.

Testowanie eksploracyjne

W testowaniu eksploracyjnym tester może dowolnie badać system w poszukiwaniu błędów, o których nikt wcześniej nie pomyślał. Ten rodzaj testowania jest przydatny, gdy naszym celem jest znalezienie tak dużej liczby błędów, jak to tylko możliwe. W wielu przypadkach osoba testująca, nawet gdy realizuje zaplanowany test, nie tylko ma zupełną swobodę, ale nawet powinna się dodatkowo rozglądać i obok testów zaplanowanych wykonywać testowanie eksploracyjne.

Ludzie często myślą, że skoro testy automatyczne mogą wykonywać się szybko i w krótkim okresie czasu pokrywać dużą część oprogramowania, to pozwalają one na szybsze wyszukanie dużej ilości błędów poprzez losowe lub systematyczne próby pokrycia wielu przypadków użycia. Tych, którzy również tak sądzą, muszę rozczarować, bo niestety nie to jest podstawowym celem testowania automatycznego. Aby być w stanie znaleźć błędy, test powinien wiedzieć, czego należy się spodziewać, a czego nie. O ile tester manualny posiada taką intuicyjną wiedzę, to maszyna już nie. Jeśli wydaje nam się, że możemy sformalizować te reguły w prosty sposób, który można zautomatyzować, to powinniśmy pomyśleć jeszcze raz. W większości przypadków reguły te są tak skomplikowane jak sam testowany system... Pamiętajmy, że głównym celem testowania automatycznego nie jest znalezienie tak dużej liczby błędów, jak to tylko możliwe, ale raczej dostarczenie szybkiej informacji zwrotnej o tym, czy system zachowuje się w oczekiwany sposób, jak zostało to zdefiniowane w testach.

Z drugiej strony istnieją przypadki, w których możemy sformalizować pewne istotne (choć bardzo ogólne) reguły na temat granic możliwych rezultatów systemu i utworzyć test automatyczny, który sprawdzi wiele możliwych wejść – losowo lub sekwencyjne – i sprawdzi, czy uzyskiwane rezultaty faktycznie mieszczą się w oczekiwanym przedziale. Jeśli spróbujemy napisać lub zmodyfikować takie testy w celu zweryfikowania pewnych nietrywialnych reguł, wówczas test taki bardzo szybko stanie się na tyle skomplikowany, że trudno będzie nam go dalej analizować i utrzymywać. Z tego powodu z techniki tej będziemy chcieli korzystać jedynie w celu weryfikacji reguł, które są proste do zdefiniowania, ale mają zasadnicze znaczenie. We wszystkich pozostałych przypadkach należy stosować proste testy, które weryfikują tylko jeden lub kilka konkretnych przykładów. Technika ta nazywana jest testowaniem opartym na własnościach (property-based testing), a najbardziej rozpoznawalnym narzędziem, które ją obsługuje, jest QuickCheck, napisany pierwotnie w języku programowania Haskell, a później przeniesiony do wielu innych popularnych języków, takich jak Java, F# (który może być wykorzystywany przez C# i inne języki platformy .NET), Python, Ruby, JavaScript, C/C++ i wiele innych. Ponieważ temat ten odnosi się jedynie do rzadkich przypadków, wykracza on poza zakres tej książki.

Kolejną i prawdopodobnie bardziej przydatną opcją jest tworzenie półautomatycznych testów lub narzędzi dla testerów manualnych, które pomogą im szybciej pokrywać wiele przypadków. Samą analizę zgodności uzyskiwanych rezultatów z oczekiwaniami pozostawia się w rękach testerów manualnych. Informacje na temat tego, kiedy tworzyć i gdzie używać takich narzędzi, trzeba znaleźć samodzielnie, ponieważ ten temat również wykracza poza zakres tej książki. Tak więc od tej pory, jeśli nie zostanie to określone inaczej, będziemy mówić wyłącznie o zaplanowanym testowaniu manualnym oraz jego automatyzacji.

MAŁPIE TESTOWANIE – AUTOMATYCZNE TESTOWANIE EKSPLORACYJNE

Termin małpie testowanie (monkey testing) odnosi się do praktyki losowego wciskania klawiszy (lub wykonywania operacji bez rozumienia ich kontekstu) – jak mogą to robić małpy lub małe dzieci – w celu sprawdzenia, czy dany program ulegnie awarii, czy też nie. Choć technikę tę można w łatwy sposób zautomatyzować, to jednak z kilku powodów nie będzie ona zbyt efektywna:

Możemy w ten sposób wyłapywać jedynie awarie (lub konkretne błędy, których poszukujemy), a nie błędy innego rodzaju, ponieważ nie możemy zdefiniować oczekiwanego rezultatu dla każdej akcji. Nawet jeśli program zawiesi się (ale nie ulegnie awarii), to prawdopodobnie nie będziemy w stanie tego wykryć, nie mówiąc już o ustaleniu, czy aplikacja zachowuje się w sensowny sposób.

Ponieważ automatyzacja powoduje wciskanie klawiszy na klawiaturze i przycisków myszy na ślepo, szanse na jakieś interesujące rezultaty są dosyć niskie. Przykładowo może ona utknąć na kilka godzin przy otwartym oknie dialogowym, dopóki nie wciśnie losowo klawisza „Enter” lub „Esc” bądź też nie kliknie przycisku „OK”. Oczywiście możemy stworzyć nieco bardziej inteligentną „małpę”, która zamiast wysyłać losowe zdarzenia wciśnięcia klawisza, będzie klikać wyłącznie dostępne przyciski i elementy menu. W ten sposób będziemy w stanie rozwiązać wspomniany problem z konkretnym oknem dialogowym, ale dowolny inny formularz lub okno dialogowe z walidacją wejścia prawdopodobnie spowoduje ten sam problem.

Rozważania dotyczące testowania automatycznego

Teraz, gdy rozumiemy już, że testy automatyczne nie nadają się zbytnio do testowania eksploracyjnego, spójrzmy w jaki sposób planowane testowanie manualne różni się od testowania automatycznego. W poniższych punktach przeanalizujemy podstawowe różnice pomiędzy nimi oraz wpływ, jaki powinny mieć na nasze decyzje, gdy przyjdzie nam zaplanować test automatyczny w przeciwieństwie do planowania testu manualnego.

Dokładność

(Planowane) testy manualne pisane są przez ludzi do wykorzystywania (czytania i wykonywania) przez ludzi. Co więcej, ich użytkownikami są zwykle inni członkowie zespołu, którzy znają aplikację i dziedzinę biznesu, a przy tym mają takie same założenia dotyczące systemu i sposobu jego wykorzystywania. Mówimy tu o „innych” członkach zespołu, a więc o bardziej optymistycznym przypadku, mimo że w rzeczywistości większość przypadków testowych wykonuje ta sama osoba, która je napisała. W takim wypadku te podstawowe założenia nigdy nie są kwestionowane i przypadki testowe⁹ zawierają jedynie te szczegóły, które zdaniem autora testu będą mu potrzebne, aby sobie przypomnieć, co zamierzał zrobić podczas tworzenia przypadku testowego.

Wszystkie te założenia, których dokonuje autor przypadku testowego, wprowadzają pewną niejasność. Ludzie zwykle radzą sobie bez problemu z odrobiną niejasności, ale komputery już nie. W przypadku pisania testów automatycznych nie ma miejsca na wprowadzanie żadnych niejasności. W końcu test automatyczny (podobnie jak każdy inny kod komputerowy) musi być precyzyjny i szczegółowy, aby komputer był w stanie go wykonać. Bardzo często podczas konwertowania testów manualnych na automatyczne pojawia się wiele różnych pytań i to nawet takich, które dotyczą – jak by się mogło wydawać – mało istotnych szczegółów. Aby jednak móc zautomatyzować test, musimy znaleźć odpowiedź na każde takie pytanie. Odpowiedź ta zostanie zawarta w kodzie automatyzacji testu i będzie wykorzystywana przy każdym wykonaniu testu! Tak naprawdę pytania te często ujawniają więcej interesujących i istotnych błędów niż jest rozpoznawanych w wyniku samego wykonania testu automatycznego.

Łatwość utrzymania

Jak wspomnieliśmy w rozdziale 1, nie ma sensu wykonywać testów dla kodu, który nie uległ żadnej zmianie, więc należy zawsze oczekiwać, że prawie przy każdym cyklu testowania aplikacja była modyfikowana. Oznacza to, że przypadki testowe muszą być często modyfikowane w celu odzwierciedlenia zmian dokonanych w aplikacji. W wypadku manualnych przypadków testowych dzieje się to bardzo rzadko. W większości przypadków zmiany w aplikacji są niewielkie, a osoba wykonująca test może łatwo się zorientować, co uległo zmianie i w jaki sposób powinna odnieść to, co zostało napisane w przypadku testowym, do stanu faktycznego. Jednak jak już wspomniano wcześniej, w przypadku automatyzacji testów liczy się każdy mały szczegół. Z tego powodu testy automatyczne muszą być stale aktualizowane, aby odzwierciedlić każdą zmianę, która może potencjalnie wywrzeć na nie wpływ. Załóżmy przykładowo, że nasza aplikacja zawiera polecenie Zapisz w menu Plik, a jeden z naszych kroków w przypadku testowym określa, że powinniśmy „wybrać polecenie Zapisz z menu Plik”. Jeśli później polecenie Zapisz zostanie przeniesione poza menu Plik do postaci lepiej widocznego przycisku na pasku narzędzi, wówczas każdy rozsądny tester będzie wiedział, że krok ten powinien się teraz odnosić do przycisku na pasku narzędzi zamiast do elementu menu, nawet jeśli sam opis tego kroku się nie zmienił. Jeśli jednak test automatyczny nie będzie w stanie znaleźć wskazanego w teście elementu menu „Zapisz”, wówczas test taki zakończy się niepowodzeniem.

Gdy rozumiemy już, że testy wymagają stałego utrzymywania, najważniejszym pytaniem jest to, w jaki sposób powinniśmy pisać testy automatyczne, abyśmy potem mogli w szybki i prosty sposób wprowadzać te zmiany. Odpowiedzi na te pytania znajdują się w większości rozdziałów II części tej książki.

Wrażliwość na zmianę – dokładność wraz z łatwością utrzymania

Bazując na tym, co powiedzieliśmy wcześniej o dokładności, można by pomyśleć, że skrypty¹⁰ testów automatycznych powinny, a nawet muszą, być wypełnione bardzo precyzyjnymi szczegółami dotyczącymi każdej operacji, jaką powinny wykonywać. Z drugiej strony, im bardziej opieramy się na takich szczegółach, tym trudniejsze staje się utrzymywanie naszych skryptów testowych w aktualnym stanie. Wydaje się więc, że ograniczenia te stoją ze sobą w sprzeczności i że nie jesteśmy w stanie zrealizować obu z nich.

Na szczęście dokładność nie oznacza koniecznie zaśmiecania każdego skryptu wszystkimi drobnymi szczegółami. Wszystkie te szczegóły muszą gdzieś zostać zdefiniowane, ale nie wszystkie muszą znajdować się wewnątrz samych skryptów. System automatyzacji testów jest zwykle czymś więcej niż tylko zwykłą kolekcją skryptów testowych, ale może – i powinien – być zbudowany w sposób modułowy, tak że niektóre fragmenty zawierają drobne szczegóły, a skrypty są jedynie złożeniem tych fragmentów.

Możemy je postrzegać jako plany (rysunki techniczne) samochodu. Podczas projektowania samochodu nie istnieje pojedynczy rysunek zawierający wszystkie jego szczegóły. Samochód jest złożonym obiektem, który składa się wielu mniejszych elementów (podwozie, nadwozie, silnik, skrzynia biegów, układ sterowania, koła, części wewnętrzne itd.), a każdy z nich złożony jest z jeszcze mniejszych części. Istnieje zapewne jeden rysunek, który daje nam „pełny obraz” samochodu, ale z dużo mniejszą ilością szczegółów, a także wiele mniejszych rysunków, które opisują szczegóły każdej części z osobna. Gdyby wszystkie te szczegóły zostały zawarte w jednym szczegółowym rysunku, a inżynier projektujący siedzenia chciał w dokonać w nim zmiany (niemającej wpływu na jego zewnętrzne wymiary), wówczas trzeba by zaktualizować cały rysunek!

Podobnie jest przy tworzeniu testów automatycznych: mimo że wszystkie szczegóły trzeba zdefiniować przed wykonaniem testu, jednak nie powinny one znajdować się w tym samym miejscu, ale należy je rozłożyć na kilka komponentów (metody, klasy, moduły itd.), które można modyfikować lub zastępować innymi, bez wpływu na pozostałe elementy.

Obsługa niepowodzeń

Przy pierwszym wykonaniu zaplanowanego przypadku testowego możemy napotkać wiele nieoczekiwanych warunków, o których nie pomyśleliśmy podczas pisania tego przypadku. Przy odpowiednio dobrej organizacji prawdopodobnie będziemy w stanie naprawić ten przypadek testowy po tym pierwszym uruchomieniu. Podobny proces następuje podczas tworzenia testu automatycznego, dopóki test ten przynajmniej raz nie zakończy się sukcesem.

Ale po tym etapie, bez względu na to, czy mówimy o testach manualnych czy automatycznych, nadal mogą się pojawiać nieoczekiwane warunki, które mogą być spowodowane:

1. Nowym błędem w produkcie (regresja)

2. Uzasadnioną zmianą (usprawnieniem) w produkcie, której nie byliśmy świadomi.

3. Problemem środowiskowym, takim jak awaria sieci, brak dostępnej pamięci itd.

4. Zdarzeniem w produkcie, którego obsługa nie została uwzględniona w teście. Załóżmy przykładowo, że codziennie o 16:00 aplikacja wyświetla okienko wyskakujące z komunikatem przypominającym użytkownikowi o konieczności utworzenia kopii zapasowej jego pracy. Test automatyczny zawsze kończy się powodzeniem, z wyjątkiem sytuacji, gdy zostanie on uruchomiony krótko przed godziną 16:00, kiedy prezentowana wiadomość może sprawić, że test zakończy się niepowodzeniem. Jest to oczywiście dosyć prosty przykład, ale rzeczywiste aplikacje zawierają zwykle złożony kod, która uniemożliwia prześledzenie wszystkich warunków, jakie mogą wystąpić, oraz ich odpowiednie obsłużenie w przypadku testowym. Można powiedzieć, że te luki w projekcie przypadku testowego są tak naprawdę błędami samego przypadku testowego. W przypadku testów automatycznych możemy nazywać je… błędami w testach!

5. Ktoś zrobił coś z systemem przed lub w czasie trwania testu, co w sposób niezamierzony wpłynęło na przebieg lub rezultat tego testu. Ten „ktoś” może być kolejnym testerem manualnym, który uruchomił jakieś testy, użytkownikiem lub administratorem, który zmieniał jakieś ustawienia, bądź też innym testem automatycznym, który wykonał jakieś działania. Jeśli na przykład jeden test zmienia hasło dla konta użytkownika, za pośrednictwem którego drugi test próbuje się zalogować, wówczas ten drugi może zakończyć się niepowodzeniem. Kolejnym przykładem jest sytuacja, w której dwa testy są wykonywane jednocześnie na tym samym serwerze i każdy z nich próbuje zmienić pewne dane, które są wykorzystywane przez ten drugi test. Ta klasa problemów nazywana jest problemami izolacji (isolation problems) i są one zbliżone do poprzedniego rodzaju problemów, ale przynajmniej w przypadku testów automatycznych wskazują one zwykle nie tyle na błąd w określonym teście, co raczej problem w ogólnej architekturze infrastruktury testowania. W rozdziałach 6 i 7 omówiono te problemy bardziej szczegółowo.

Choć wszystkie te warunki możemy napotkać zarówno podczas wykonywania testu manualnego, jak i uruchamiania testu automatycznego, to sposób ich obsługi stanowi kluczową różnicę między testami manualnymi i automatycznymi. Ludzie (testerzy manualni) zwykle łatwo rozróżniają te warunki i wiedzą, w jaki sposób obsłużyć każdą z nich. Nawet w przypadku znalezienia błędu w produkcie, po tym jak tester zgłosi ten błąd, w większości przypadków może on kontynuować wykonywanie pozostałej części przypadku testowego, na przykład po zastosowaniu jakiegoś obejścia lub po ponownym wykonaniu kilku ostatnich kroków. Z drugiej strony, w kontekście automatyzacji słowo „nieoczekiwane” oznacza, że komputer nie wie, w jaki sposób je obsłużyć!

Ważna uwaga

Automatyzacja testów może do pewnego stopnia obsłużyć trzeci rodzaj przyczyn niepowodzeń, ale jest to bardzo delikatny temat. Jeśli możemy zidentyfikować potencjalne zdarzenia, które mogą wystąpić podczas wykonywania testu, możemy być w stanie obsłużyć lub obejść je w kodzie w sposób, w jaki zrobiłby to użytkownik (lub tester manualny). Jednakże należy tego dokonywać z rozwagą, ponieważ z jednej strony celem tych obejść jest uczynienie testów bardziej niezawodnymi, ale z drugiej strony znacznie trudniej jest zweryfikować, czy test sam w sobie poprawnie obsługuje wszystkie te sytuacje, co może dać efekt odwrotny od zamierzonego: testy będą mniej deterministyczne i ostatecznie mniej wiarygodne! Mimo że w niektórych przypadkach obejścia te są warte zachodu, to powinniśmy wziąć pod uwagę alternatywy omawiane w rozdziałach 6 i 7.

CZY TESTY KOŃCZĄCE SIĘ NIEPOWODZENIEM NALEŻY POWTARZAĆ?

Niektóre osoby wbudowują do swoich bibliotek testowania mechanizmy ponawiające, które kilkukrotnie powtarzają wszystkie testy zakończone porażką i oznaczają je jako zakończone niepomyślnie dopiero wtedy, gdy żadna z tych prób nie zakończyła się sukcesem. Jednak według mnie popełniają oni w ten sposób błąd. Każde niepowodzenie testu daje nam jasny sygnał: albo mamy jakiś problem z kodem testu, albo z testowaną aplikacją. Choć na początku może to być bardzo czasochłonne, problemy te powinny być szczegółowo analizowane w celu znalezienia ich głównej przyczyny i odpowiednio obsłużone, aby nie doszło do ich ponownego wystąpienia. Ignorowanie tych niepowodzeń poprzez ślepe powtarzanie całego testu prawdopodobnie sprawi, że nasza automatyzacja stanie się nierzetelna i będzie potencjalnie zostawiać w naszym produkcie ważne niedeterministyczne błędy! Nie mówiąc już o dodatkowym czasie, potrzebnym do ponownego wykonywania tych niepomyślnych testów…

Długość przypadku testowego

Różnica pomiędzy sposobem obsługiwania nieoczekiwanych warunków w testach manualnych, a tym, jak robią to testy automatyczne, ma olbrzymi wpływ na sposób, w jaki powinniśmy pisać automatyzację: poszczególne przypadki testów manualnych są często dosyć długie i zwykle w całości pokrywają kompletną funkcję wraz ze wszystkimi jej niuansami. Sensowne wydaje się, aby przypadki testów manualnych weryfikowały „przy okazji” wiele mniejszych rzeczy, oszczędzając w ten sposób czas podczas wykonywania przypadku testowego. Jeśli jakiś pomniejszy błąd lub coś, co zostało zmienione, wpłynie na te poboczne weryfikacje, tester manualny będzie mógł je pominąć i kontynuować pozostałą część przypadku testowego. Jeśli jednak zautomatyzujemy taki dłuższy przypadek testowy bez żadnych zmian i zakończy się on niepowodzeniem już przy jednej z pierwszych weryfikacji, to nie będzie on w stanie zdecydować, czy należy kontynuować działanie, czy też nie.

Niektóre biblioteki automatyzacji pozwalają nam zgłosić błąd i kontynuować wykonywanie testu. Kiedy to jednak osoba testująca napotka błąd, zwykle – na podstawie rozeznania natury danego problemu – podejmuje ona decyzje, czy należy kontynuować test, cofnąć się o kilka kroków (i o ile dokładnie), czy też może całkowicie przerwać jego wykonywanie. Uważam, że podejmowanie w czasie wykonywania testu decyzji dotyczącej sensu jego kontynuowania (bez powtarzania lub obchodzenia kilku ostatnich kroków) wyłącznie na podstawie ważności samej weryfikacji nie jest miarodajne i w konsekwencji może negatywnie wpłynąć na wiarygodność automatyzacji testów jako całości! W szczególności zagwarantowanie, że testy zachowują się poprawnie we wszystkich możliwych warunkach niepowodzenia, jest prawie niemożliwe.

W innych bibliotekach (w tym zdecydowanej większości bibliotek testów jednostkowych) stosowane jest podejście, w którym każde nieoczekiwane warunki napotykane przez test powodują niepowodzenie całego przypadku testowego i przejście do wykonywania kolejnego przypadku testowego zamiast do kolejnego kroku. W mojej opinii jest to najbezpieczniejszy i najbardziej niezawodny sposób. Oznacza to jednak, że testy muszą być krótkie i powinny weryfikować wyłącznie jedną rzecz, gdyż w przeciwnym wypadku nawet najmniejsze niepowodzenie może zablokować wykonanie dużo istotniejszych fragmentów testu. Jeśli spróbujemy sprawić, aby nasze testy mądrzej obsługiwały możliwe porażki, tylko pogorszymy tę sprawę, ponieważ nasz kod testu będzie miał teraz rozwiniętą logikę, której nie można żaden racjonalny sposób zweryfikować!

Oznacza to również, że prawie każda weryfikacja powinna mieć swój własny przypadek testowy! Może się to wydawać nieekonomiczne i uciążliwe, ale w dłuższej perspektywie zdamy sobie sprawę, że jest to jedyny sposób, aby nasze testy były niezawodne i łatwe w utrzymaniu.

Zależności pomiędzy testami

Przypadki testów manualnych są czasami opisywane są z uwzględnieniem zależności między nimi: wykonaj test X dopiero po wykonaniu testu Y. Ponieważ w przypadku testów automatycznych niepowodzenie w jednym teście zazwyczaj przerywa ten test i uruchamia kolejny, nie chcemy, aby jakakolwiek porażka miała wpływ na kolejne testy. Musimy więc jakoś zagwarantować, że każdy test będzie się rozpoczynał od początkowego, dobrze znanego nam stanu. Innymi słowy, zależności pomiędzy testami automatycznymi są zdecydowanie odradzane. Szczegóły dotyczące różnych opcji wymuszania czystego startu każdego testu są omawiane w rozdziale 7.

Rejestrowanie i zbieranie dowodów

Sposób odzyskiwania sprawności przez automatyzację po napotkaniu nieoczekiwanych warunków, który pozwala przejść do wykonywania kolejnego przypadku testowego, to jedno, ale równie istotne jest to, co należy zrobić z tymi nieoczekiwanymi warunkami. Jeśli podczas wykonywania testu manualnego osoba testująca napotka nieoczekiwany warunek i będzie przekonana, że problem leży w kodzie aplikacji, zwykle od razu zgłosi ten błąd, zanim przejdzie do wykonywania pozostałej części przypadku testowego lub przejdzie do następnego przypadku. W samym raporcie o błędzie zwykle opisze kroki, których wykonanie doprowadziło do wystąpienia błędu, dodając niekiedy pewne dodatkowe fakty, które jej zdaniem mogą być istotne. Podczas pisania takiego raportu osoba testująca powinna również spróbować prześledzić naturę tego błędu poprzez proste „eksperymentowanie z nim” w celu poznania jego granic.

Sytuacja wyglądać będzie całkiem inaczej, kiedy to test automatyczny napotka nieoczekiwany warunek:

Jak już powiedzieliśmy, testy automatyczne traktują każdy nieoczekiwany warunek jako niepowodzenie, bez odpowiedniej możliwości wyrażenia natury problemu.

Testy automatyczne zwykle wykonywane są bez nadzoru, a badania dotyczące ich niepowodzeń wykonywane są już po ich zakończeniu. Oznacza to, że gdy można przeprowadzić badanie dotyczące niepowodzenia, część dowodów jest już utracona lub uszkodzona!

Jeśli test w każdym środowisku i za każdym razem odtwarza ten sam błąd, możemy uruchomić go ponownie w innym środowisku (np. na maszynie lokalnej, jeśli błąd wystąpił w kompilacji nocnej lub kompilacji ciągłej integracji), lub wykonać jego kroki ręcznie i w ten sposób zbadać ten błąd. Nawet w takim przypadku prawdopodobnie zajmie to cenny dodatkowy czas. Niemniej jednak w przypadku, gdy błąd nie występuje cały czas, niezwykle istotne jest, aby dysponować dziennikami – zarówno dla testu, jak i aplikacji – jak również dowolnymi innymi dowodami, które mogą pomóc zbadać dany problem. Dowodem takim może być zrzut lub zapis wideo ekranu, migawki bazy danych, źródło HTML strony internetowej itd. Temat badania testów kończących się niepowodzeniem omawiany jest bardziej szczegółowo w rozdziale 13.

CZY SYSTEM AUTOMATYZACJI TESTÓW POWINIEN AUTOMATYCZNIE ZGŁASZAĆ BŁĘDY?

Choć widziałem wiele prób podłączania systemów automatyzacji testów bezpośrednio do systemów raportowania błędów oraz automatycznego raportowania błędów w przypadku zakończenia testu niepowodzeniem, to nie wydaje mi się to być zbyt dobrym pomysłem. Jak już wspomnieliśmy, wszystkie nieoczekiwane warunki mogą powodować, że testy automatyczne będą kończyć się niepowodzeniem, ale nie wszystkie te porażki są tak naprawdę błędami. Ale nawet jeśli błędy te zostaną przypisane testerowi w celu ich zbadania, to istnieje wiele przypadków, w których pojedynczy błąd spowoduje, że wiele innych testów zakończy się niepowodzeniem, co wprowadzi dodatkowy narzut związany z zarządzaniem i śledzeniem tych wszystkich automatycznie generowanych błędów. Więcej szczegółów na temat zalecanego sposobu, w jaki powinny być traktowane błędy wykrywane przez automatyzację, można znaleźć w rozdziałach 5 i 15.

Zaufanie

Brak zaufania pomiędzy programistami i testerami manualnymi jest (niestety) dosyć częsty: testerzy obwiniają programistów o pisanie niechlujnego kodu, programiści obwiniają testerów o zgłaszanie błędów ze zbyt małą ilością informacji lub niedokładnymi danymi itd. (a wszyscy winią menedżerów projektów za pisanie niejasnych wymagań, ale to już inna historia… w rozdziałach 5 i 16 pokazujemy, że i ten problem może nam pomóc rozwiązać metodyka ATDD). Ostatecznie jednak wszyscy zgadzają się, że ta druga rola jest istotna i niezbędna.

Jeśli chodzi o automatyzację testów, to zarówno programiści, jak i testerzy, a także ich menedżerowie, muszą ufać maszynie. Z początku może się to wydawać oczywiste: maszyny zawsze wytwarzają spójne rezultaty, więc są lepsze od ludzi! Dlaczego zatem trudno im zaufać? Ale jak już wspomniano wcześniej, testy automatyczne mogą kończyć się niepowodzeniem z wielu różnych powodów, a nie tylko w wyniku napotkania błędów. Tak naprawdę abyśmy mogli ufać testom automatycznym, musimy wierzyć, że:

Testy kończą się niepowodzeniem tylko w przypadku napotkania prawdziwych błędów.

Testy zawsze wykrywają błędy

Bez względu na to, jak bardzo byśmy tego chcieli i próbowali urzeczywistnić te twierdzenia, to nie możemy ich zagwarantować. Ale za pomocą dobrego zestawu testów automatycznych możemy zapewnić ich łagodniejszą wersję:

Testy kończą się niepowodzeniem zwykle z powodu istnienia prawdziwych błędów (i dosyć łatwo jest zbadać i ustalić ich prawdziwą przyczynę)

Testy zawsze wykrywają błędy, do wyłapywania których zostały zaprojektowane.

Jeśli nasze testy automatyczne będziemy projektować tak, aby były krótkie i proste, co powinniśmy robić, wówczas dosyć łatwo będzie nam udowodnić to drugie twierdzenie. Ale udowodnienie pierwszego twierdzenia będzie już trudniejsze. Sytuacja, w której twierdzenie to jest fałszywe, objawia się wtedy, gdy istnieje wiele testów, które przez długi okres czasu kończą się niepowodzeniem, mimo że podstawowa funkcjonalność weryfikowana przez te testy działa prawidłowo, lub też gdy testy często kończą się niepowodzeniem z niewyjaśnionych powodów. Gdy tak się dzieje, interesariusze (zwłaszcza menedżerowie) przestają ufać wynikom testów automatycznych. Gdy rezultaty testów automatycznych są ignorowane, a do rozwiązania tych problemów nie są podejmowane żadne środki, testy dosyć szybko stają się nieistotne i nieaktualne, zaprzepaszczając wszelkie inwestycje, które zostały poczynione na budowę systemu automatyzacji testów!

Niestety istnieje duży odsetek projektów automatyzacji testów, które rozpoczynane są z dużym entuzjazmem, ale po pewnym czasie przestają spełniać swoje obietnice i chwilę później marnie kończą. Miejmy nadzieję, że książka ta pomoże nam uniknąć tego przeznaczenia i poprowadzi nas w kierunku sukcesu!

Zanim zagłębimy się dalej w aspekty związane z zapewnianiem sukcesu projektom automatyzacji testów, chciałbym jeszcze wspomnieć o pewnych kluczowych praktykach, dzięki których nasz projekt automatyzacji uniknie unicestwienia i osiągnie sukces:

1. Wszystkie niepowodzenia, a już na pewno błędy automatyzacji, muszą zostać obsłużone i naprawione tak szybko, jak to tylko możliwe (więcej informacji na ten temat można znaleźć w rozdziałach 5 i 15).

2. Każde niepowodzenie powinno zostać dokładnie zbadane, aby znaleźć jego główną przyczynę. Co prawda „przykrywanie” błędów może nam na krótką metę pomóc rozwiązać niektóre problemy, ale w przyszłości może powodować kolejne, trudniejsze do zidentyfikowania i naprawienia (więcej informacji na ten temat można znaleźć w rozdziale 13).

3. Testy powinny być tworzone w sposób gwarantujący spójne wyniki. Jeśli rezultaty zależą od warunków zewnętrznych, to w wypadku niepowodzenia istnieje tendencja do obwiniania warunków zewnętrznych i unikania badania prawdziwej przyczyny (więcej informacji na ten temat można znaleźć w rozdziałach 6 i 7).

Rozdział 3. Ludzie i narzędzia

Większość klientów zatrudniających mnie w roli konsultanta, który ma im pomóc zacząć korzystać z automatyzacji testów, zadaje na początku pytanie: „Jakie narzędzia są dostępne?” i „Z których narzędzi powinniśmy korzystać?”. Jeśli sami chcemy zacząć korzystać z automatyzacji testów, to prawdopodobnie również zadajemy sobie te pytania. Krótka odpowiedź na pierwsze pytanie jest taka, że istnieje bazylion narzędzi do automatyzacji testów. Oczywiście jest jeszcze Selenium, tak więc jest ich bazylion i jeden. Z kolei krótką odpowiedzią na drugie pytanie jest klasyczna odpowiedź konsultanta: „To zależy”.

Wybieranie właściwych narzędzi

Teraz już w nieco poważniejszym tonie: mimo że naprawdę dostępnych jest wiele narzędzi do automatyzacji testów i prawie każdego dnia słyszymy o jakimś nowym (a każde z nich obiecuje być „tym właściwym”), to istnieje tylko kilka kategorii narzędzi przeznaczonych do różnych celów. Niektóre narzędzia realizują więcej niż jeden cel, przy czym w większości wypadków będziemy prawdopodobnie potrzebować kombinacji pewnych narzędzi. Aby dowiedzieć się, które z tych narzędzi będą dla nas najbardziej odpowiednie, powinniśmy najpierw odpowiedzieć sobie na kilka pytań. O ile pytanie „którego narzędzia powinienem użyć” jest pytaniem typu „jak”, to pytania, od których powinniśmy zacząć, są pytaniami typu „dlaczego” i „co”. Gdy już sobie na nie odpowiemy, wybór właściwych narzędzi powinien być w większości wypadków dosyć trywialny. Kategorie tych narzędzi oraz pytania, na które powinniśmy uzyskać odpowiedź, omawiamy w dalszej części tego rozdziału. Zanim jednak podejmiemy konkretną decyzję, powinniśmy przynajmniej przeczytać rozdziały z części I tej książki, ponieważ pomogą nam one uzyskać lepsze odpowiedzi na te pytania.

Podczas gdy pozostałe rozdziały z części I tej książki pomogą nam odpowiedzieć na większość pytań typu „dlaczego” i „co”, ten rozdział poświęcony jest jednemu istotnemu pytaniu, które jest dosyć często pomijane. Pytanie to nie jest pytaniem typu „dlaczego”, „co” ani też „jak”, ale raczej pytaniem typu „kto”…

Kto powinien pisać testy?

W większości przypadków klienci znają już odpowiedź na to pytanie, mimo że nie wzięli oni pod uwagę wszystkich dostępnych alternatyw i ich konsekwencji, gdyż po prostu nie są oni świadomi ich istnienia! Opiszmy więc teraz dostępne opcje i związane z nimi konsekwencje. Zwróćmy uwagę, że nie ma jednej właściwej odpowiedzi na to pytanie, a każda opcja ma swoje własne wady i zalety, będziemy zatem sami musieli podjąć taką decyzję, która najlepiej będzie pasowała do naszej organizacji.

Nawet jeśli mamy już w swojej organizacji zespół zajmujący się automatyzacją, to nadal powinniśmy przeczytać ten rozdział, gdyż pozwoli nam to lepiej zrozumieć wady i zalety sytuacji, w której teraz jesteśmy, dzięki czemu łatwiej nam będzie się z nimi zmierzyć. Na dłuższą metę możemy nawet sami rozważyć lub przynajmniej spróbować nakłonić naszych menedżerów do zmiany podjętej decyzji.

Promowanie testerów manualnych lub niedoświadczonych deweloperów do rangi deweloperów automatyzacji

Czasem testerzy manualni, którzy mają zerowe lub bardzo niewielkie umiejętności programistyczne, dowiadują się o jednym z narzędzi automatyzacji typu „nagraj i odtwórz” i są nim niezwykle podekscytowani. Udają się więc do swojego szefa i mówią mu, że mogą szybko zacząć tworzyć testy automatyczne i zaoszczędzić w ten sposób sporo czasu i pieniędzy! Taki entuzjazm jest świetny i jako menedżerowie możemy zechcieć go wykorzystać, ale powinniśmy pamiętać o tym, o czym mówiliśmy na początku rozdziału 1: narzędzia typu „nagraj i odtwórz” są łatwe na początku, ale w dłuższej perspektywie się nie sprawdzają.

Wielu testerów manualnych ma jednak pewne doświadczenie w programowaniu. Niektórzy z nich studiowali nawet informatykę lub podobną dziedzinę, ale na kilka lat wylądowali w zespole zapewniania jakości (Quality Assurance, QA). Inni po prostu bawili się programowaniem i cieszą się na myśl o pisaniu kodu. Takie osoby często postrzegane są jako idealni kandydaci do rozpoczęcia prac związanych z automatyzacją testów. Przydzielenie tego zadania komuś, kogo już znamy i komu ufamy, a kto dodatkowo posiada pewne umiejętności w programowaniu i zna nasz system oraz organizację, może być bardzo przekonujące. Nie ma potrzeby inwestować zbyt wiele w szkolenia, a takie zadanie zwykle mocno motywuje tę osobę! Na początku menedżer zespołu QA prawdopodobnie zdecyduje, że osoba ta na projekt automatyzacji testów poświęcać będzie jedynie od 20% do 50% czasu swojej pracy, natomiast przez resztę czasu nadal będzie testować oprogramowanie ręcznie.

Oczywiście każda osoba jest inna, a my dokonaliśmy tu pewnego uogólnienia, tak więc słowa te powinniśmy traktować trochę z przymrużeniem oka i sami decydować o własnych sprawach. O ile jednak niektóre z tych osób mogą nadawać się na członków zespołu automatyzacji testów, gdy zespół taki zostanie już odpowiednio utworzony, o tyle zwykle nie będą oni właściwymi osobami do rozpoczynania budowy i opracowywania systemu automatyzacji testów. Jeśli im na to pozwolimy, to prawdopodobnie wstępny etap ich pracy zakończy się sukcesem, co zasugeruje nam, że dokonaliśmy właściwego wyboru. Jednak w miarę upływu czasu zaczną pojawiać się problemy z utrzymaniem i stabilnością, przez co stan tego projektu może zacząć się pogarszać.

W początkowym etapie tworzenie testów automatycznych, które „wykonują jakąś pracę”, nie stanowi zwykle zbyt dużego wyzwania technicznego. Niektóre narzędzia pozwalają to osiągnąć nawet osobom, które nie mają żadnego doświadczenia w programowaniu, ale nawet pisanie działających testów automatycznych w kodzie (np. za pomocą narzędzia Selenium) nie wymaga zwykle dużych umiejętności programistycznych. Takie testy zwykle będą kończyć się sukcesem i mogą nawet znaleźć jakieś interesujące błędy.

Jednak po pewnym czasie niektóre rzeczy mogą zacząć sprawiać nam problemy: rozwój aplikacji nie stoi przecież w miejscu. Aplikacja ewoluuje, dodawane są nowe funkcje, niektóre istniejące funkcje i ekrany interfejsu użytkownika ulegają zmianie, a niektóre fragmenty są przepisywane. Od czasu do czasu taki niedoświadczony deweloper automatyzacji zauważy, że pewne rzeczy w aplikacji zostały zmienione i będzie musiał w odpowiedni sposób naprawić automatyzację. Jeśli będą to jedynie małe i bardzo szczegółowe zmiany, to z pewnością będzie on w stanie sobie z nimi poradzić. Ale gdy liczba testów zacznie się zwiększać, wtedy bez właściwego planowania i projektowania, oraz bez odpowiednich umiejętności debugowania, nie znajdziemy w kodzie ukrytych nieumyślne zależności lub założeń, które sprawiają, że testy są bardziej wrażliwe i mniej wiarygodne. Ponadto prędzej czy później deweloperzy aplikacji zmodyfikują coś, co może mieć wpływ na dużą liczbę testów. Naprawa tego zajmie sporo czasu, tak więc bez względu na to, czy sama aplikacja działa poprawnie czy nie, automatyzacja przez długi czas będzie całkowicie popsuta i w tym czasie nie będzie z niej żadnych korzyści.

Wyciągając wnioski z doświadczeń, taki deweloper automatyzacji może poprosić deweloperów aplikacji (angażując w to również swojego menedżera), aby informowali go zawczasu o każdej zmianie, jaką zamierzają wprowadzić, a która może mieć potencjalny wpływ na automatyzację, tak aby mógł się on do tego odpowiednio przygotować. Niestety nie będzie to działać zbyt dobrze… Nawet przy najlepszych intencjach deweloperzy nie są wystarczająco świadomi lub po prostu nie wiedzą, które z wprowadzanych przez nich zmian mogą mieć wpływ na automatyzację, a które nie. Z jednej strony przez cały czas dokonują oni bardzo dużej liczby zmian, a z drugiej – nie są na tyle zaznajomieni z tym, co i w jaki sposób jest robione w ramach automatyzacji, aby dokładnie wiedzieć, co będzie miało wpływ na automatyzację testów, a co nie.

Kolejnym problemem, jaki może się pojawić, gdy w pobliżu nie ma nikogo z odpowiednim doświadczeniem w automatyzacji testów, jest sytuacja, w której jeden lub więcej testów kończy się niepowodzeniem bez żadnego oczywistego powodu. Deweloper automatyzacji może najpierw obarczyć winą narzędzie, sieć lub po prostu brak szczęścia i spróbować uruchomić taki test ponownie. Alternatywnie może on podejrzewać, że jest to problem związany z synchronizacją w czasie, i spróbować naprawić go poprzez dodanie lub zwiększenie opóźnień pomiędzy poszczególnymi operacjami. W każdym razie uruchomi on test ponownie, a ten zakończy się sukcesem. Hura! Wkrótce jednak, nie licząc już tego, że testy stałyby się bardzo powolne w wyniku wprowadzonych opóźnień, ich ogólna stabilność będzie się pogarszać, i coraz trudniej będzie znaleźć podstawową przyczynę tych niepowodzeń. W takim wypadku ogólna korzyść, jaką powinna nam dostarczyć automatyzacja testów, będzie maleć, ponieważ ani deweloper automatyzacji, ani deweloperzy aplikacji nie będą w stanie jednoznacznie stwierdzić, czy błąd tkwi w automatyzacji, czy może w samej aplikacji. Ostatecznie w ten sposób tracimy również zaufanie do automatyzacji testów.

Jeśli deweloper automatyzacji ma pracować nad automatyzacją testów tylko częściowo (np. 20–50% swojego czasu pracy), a przez resztę czasu wykonywać testy ręcznie, to możemy natknąć się na kilka dodatkowych problemów.

Przede wszystkim poświęcanie określonej części czasu naszej pracy na jedną czynność, a reszty czasu na inną, prawie nigdy nie jest dobrym pomysłem. Niezwykle trudno jest poświęcić określoną liczbę dni w tygodniu lub długich przedziałów czasowych na jedną czynność, gdy znajdujemy się w tym samym biurze z osobami, które oczekują naszej pomocy przy jakichś innych działaniach. Jeśli nasz menedżer lub my sami nie wyznaczymy sobie konkretnych dni lub liczby godzin na realizację tego zadania, trudno nam będzie dokładnie zmierzyć, ile tak naprawdę czasu poświęcamy na automatyzację testów, a ile na wykonywanie testów manualnych, które zawsze będą mieć wyższy priorytet. Poza tym samo pisanie testów automatycznych stanowić będzie tylko część tej pracy, ponieważ powinniśmy również badać uzyskiwane wyniki i naprawiać uszkodzone testy, a to również będzie pochłaniać nasz czas. Jeśli chcemy wykonywać testy każdej nocy, to ich wyniki trzeba będzie sprawdzać każdego ranka! Jeśli nie będziemy uruchamiać testów co noc, wówczas między kolejnymi uruchomieniami testów może zostać wprowadzonych zbyt wiele zmian, przez co trudniej nam będzie dowiedzieć się, dlaczego coś się popsuło. Gdy naprawimy lub zmienimy coś w automatyzacji, to dopiero po kilku dniach będziemy w stanie stwierdzić, czy nasze zmiany były prawidłowe – ale czy do tego czasu będziemy jeszcze pamiętać, co próbowaliśmy naprawić, dlaczego i w jaki sposób? Prawdopodobnie nie… Ponadto trudno nam będzie korzystać z czegoś, co jest niespójne, i to promować. Ludzie (menedżerowie, deweloperzy itd) nie wiedzą, czy mogą się spodziewać wyników automatyzacji, czy nie, tak więc ich nie oczekują, a tym samym nie przykładają do nich zbyt dużej wagi. W rezultacie ludzie ci widzą, że nie mogą opierać się na automatyzacji testów i ostatecznie tracą do niej zaufanie.

Wniosek

Osoby z pewną wiedzą programistyczną, ale bez doświadczenia w programowaniu i automatyzacji, mogą z powodzeniem być efektywnymi członkami zespołu automatyzacji testów, ale potrzebują dobrego przewodnika! Automatyzacja testów jest sztuką (dyscypliną) samą w sobie, która wymaga posiadania określonych umiejętności. Na dobre opanowanie tej sztuki potrzebny jest czas, wysiłek i poświęcenie, tak więc nie jest to coś, co można robić „na boku”, nawet jeśli jest się najlepszym programistą na świecie.

Dzielenie pracy między testerów manualnych i deweloperów automatyzacji

Kolejne typowe podejście jest takie, że kilku doświadczonych deweloperów rozwija infrastrukturę oraz „elementy składowe”, zaś większa grupa osób nieprogramujących lub młodszych deweloperów w prosty sposób wykorzystuje tę infrastrukturę do tworzenia automatycznych skryptów testowych. Podejście to ma ścisły związek z wybranym przez nas narzędziem, którym może być albo gotowy produkt, albo narzędzie tworzone wewnątrz naszej organizacji. Ponadto rodzaj użytego narzędzia zwykle wskazuje, ile pracy można wykonać bez pisania kodu, a ile będzie wymagać kodowania.

Znam wiele zespołów, które wybrały to podejście i są z niego bardzo zadowolone. Pozwala nam ono zaangażować w projekt automatyzacji wszystkich dotychczasowych testerów z zespołu, bez konieczności uczenia ich programowania. Poza tym w rzeczywistości jest tak, że testerzy, którzy nie potrafią pisać kodu, są zwykle tańsi od programistów, więc ma to również sens pod względem finansowym.

W dalszej części tego rozdziału omawiamy bardziej szczegółowo kategorie dostępnych narzędzi, podając przy tym również kilka przykładów z wykorzystaniem tego podejścia. Większość z tych narzędzi wymusza na nas pewien ograniczony sposób interakcji z testowanym systemem w zamian za brak konieczności pisania kodu. Przykładami takich narzędzi są Ranorex® oraz SmartBear SoapUI®. Choć Ranorex działa bardzo dobrze z wieloma technologiami interfejsu użytkownika, to narzędzie to nie jest zaprojektowane do obsługi innego rodzaju testów. Z kolei SoapUI przeznaczone jest wyłącznie do testowania systemów za pośrednictwem protokołu komunikacji sieciowej HTTP (i kilku innych). Większość z tych narzędzi pozwala zarówno na nagrywanie (pojedynczych kroków lub całych scenariuszy), jak i na ręczne tworzenie i edytowanie skryptów, za pomocą intuicyjnego graficznego interfejsu użytkownika lub uproszczonego języka skryptowego, który nie wymaga prawdziwych umiejętności programistycznych. Ponadto narzędzia te dostarczają nam również pewne mechanizmy pozwalające na ponowne wykorzystywanie różnych zestawów działań, przy czym są one zwykle mniej elastyczne od prawdziwego kodu obiektowego. Narzędzia te wymagają kodowania jedynie wtedy, gdy chcemy zrobić coś, do czego nie były one projektowane. Zwykle są one dostarczane są w postaci kompletnego rozwiązania do zarządzania testami, ich wykonywania, tworzenia raportów itd.

Możliwość wielokrotnego wykorzystywania połączonych ze sobą i/lub zakodowanych akcji, nie tylko zmniejsza ilość pracy i ułatwia utrzymywanie, lecz ma jeszcze inną zaletę: dla każdego komponentu wielokrotnego użytku możemy zdefiniować opisową nazwę, która definiuje jego działanie. Pozwala nam to budować testy w sposób, który lepiej ujawnia nasze zamiary, dzięki czemu testy są łatwiejsze w utrzymaniu. Ponadto możemy wykorzystać tę technikę do zastosowania podejścia testowania opartego na słowach kluczowych (Keyword Driven Testing, KDT). W ramach tego podejścia skrypty testowe składają się wyłącznie (lub głównie) z takich elementów składowych (lub akcji), które zamiast szczegółowych działań technicznych opisują konkretne czynności biznesowe. Na przykład scenariusz zakupów online można ułożyć z takich elementów składowych jak „zaloguj”, „dodaj do koszyka” czy „zapłać”. Elementy te mogą zwykle przyjmować argumenty ze skryptu testowego i dzięki temu mogą być wykorzystywane w różnych miejscach, z różnymi wartościami lub w nieco inny sposób. Dzięki możliwości nadawania opisowych nazw automatyczne skrypty testów są bardziej czytelne oraz łatwiejsze w pisaniu i utrzymywaniu. Więc nawet gdy na taki skrypt testowy spojrzy jakiś nietechniczny przedsiębiorca, to będzie on mógł się dowiedzieć, co taki skrypt robi, bez wdawania się przy tym w jakiekolwiek szczegóły techniczne dotyczące tego, jak te działania faktycznie są wykonywane. Nazwy tych elementów składowych nazywa się czasem „słowami kluczowymi” (keywords), stąd też nazwa tej techniki.

Z jakiegoś powodu wiele organizacji samodzielnie opracowało na swoje potrzeby kompletne, złożone narzędzia obsługujące to podejście. Być może narzędzia dostępne w czasie, gdy rozpoczynali nad nimi prace, nie były dla nich do końca odpowiednie.

Jeszcze innym wariantem tego podejścia jest pisanie wszystkiego w kodzie, z zachowaniem podziału pracy pomiędzy osoby kodujące i niekodujące – koderzy tworzą „elementy składowe” w postaci metod, zaś osoby niekodujące uczone są absolutnych podstaw, które są im potrzebne do wywoływania tych metod z poziomu testów.

Mimo że u podstaw podejścia KDT leży możliwość wielokrotnego wykorzystywania elementów, jego podstawową wadą jest ogólny narzut i współzależność w procesie pisania i utrzymywania zestawu automatyzacji testów. Choć składanie skryptów testowych z predefiniowanych elementów składowych brzmi bardzo zachęcająco, to w rzeczywistości dość często istnieje potrzeba dodania lub zmodyfikowania istniejącego elementu składowego, co oznacza, że rzadko możemy napisać skrypt testowy bez uprzedniej pomocy programisty w zakresie zmiany lub dodania nowego elementu składowego. Ale zanim programista będzie mógł utworzyć nowy lub zaktualizować istniejący element, musi wiedzieć, w jaki sposób zamierzamy go używać, co często wiemy dopiero wtedy, gdy zaczniemy pisać test. Ponadto osoba niekodująca może badać niepowodzenia tylko do pewnego stopnia, bo jeśli problem tkwi wewnątrz operacji elementu składowego, to jego analizę będzie musiał kontynuować programista. Ponieważ programistów piszących elementy składowe jest zazwyczaj mniej niż testerów piszących skrypty testowe, programiści ci stają się zwykle wąskim gardłem i mogą potencjalnie opóźniać proces pisania i utrzymywania testów.

W takich sytuacjach kolejnym częstym problemem jest to, że w celu uniknięcia potrzeby dokonywania zmian w elemencie składowym, element taki projektuje się zbyt ogólnie. Objawia się to tym, że element taki przyjmuje zbyt dużą lub zbyt małą liczbę parametrów, których wartości obejmują duże ilości informacji (np. za pomocą listy oddzielanej przecinkami), co może mieć wpływ na zachowanie danej akcji. Użycie takich parametrów może faktycznie zminimalizować wymaganą liczbę poszczególnych elementów składowych, ale jest też bardzo podatne na błędy, zaś osoba pisząca skrypt powinna znać dokładny format danych oczekiwanych przez taki element. Ostatecznie tego rodzaju „rozwiązania” problemu wąskiego gardła sprawiają, że skrypty testowe stają się jeszcze bardziej złożone i trudniejsze w pisaniu i utrzymywaniu.

Istnieje jeszcze jedna kategoria narzędzi, które dodatkowo wprowadzają podział pomiędzy skryptami testowymi i ich wewnętrzną implementacją. Przykładami narzędzi z tej kategorii są Cucumber oraz SpecFlow (powiemy sobie o nich więcej w dalszej części tego rozdziału). Kategoria ta różni się od poprzedniej tym, że należące do niej narzędzia skupiają się głównie na czytelności testów, aby móc z nich korzystać głównie do dokumentacji. Zwykle narzędzia te nie są powiązane z konkretnymi technologiami, tj. możemy je łączyć z innymi narzędziami, aby dostarczać możliwości w zakresie automatyzacji interfejsu użytkownika, API HTTP (zobacz tekst uzupełniający w dalszej części tego rozdziału), komunikacji lub dowolnych innych sposobów interakcji z testowanym systemem, przy czym wymagają one większej ilości kodu niż narzędzia należące do pierwszej kategorii. Ponieważ narzędzia z tej kategorii zapewniają również podział na skrypty testowe, które nie wymagają pisania kodu, oraz na część implementacji, która jest czystym kodem (i prawdopodobnie również dlatego, że większość z tych narzędzi jest typu open source), wiele zespołów używa ich do stosowania podejścia KDT, czyli umożliwienia osobom niekodującym pisania scenariuszy testowych, a programistom pisanie elementów składowych. Niestety w ten sposób nie wykorzystują oni tych narzędzi we właściwym celu. Narzędzia te skupiają się bardziej na tym, aby scenariusze były czytelne i mogły być wykorzystywane w formie dokumentacji, niż na zapewnianiu możliwości ich ponownego użycia. Zwolennicy podejścia BDD (omawianego w dalszej części tego rozdziału) twierdzą nawet, że głównym celem tych narzędzi jest komunikowanie wymagań w sposób możliwy do zweryfikowania, i nie postrzegają oni testowania jako głównego celu tych narzędzi. Choć istnieje optymalny punkt, w którym wielokrotne wykorzystywanie i czytelność idą ze sobą w parze, to jeśli za bardzo skierujemy się w jedną stronę, zaczniemy tracić na tym drugim aspekcie. Innymi słowy, im bardziej będziemy się starać, aby nasze elementy składowe były wielokrotnego użytku, tym bardziej będą tracić one na czytelności, i odwrotnie.

Korzystanie z dedykowanego zespołu automatyzacji

Prawdopodobnie najbardziej rozpowszechnionym podejściem jest posiadanie dedykowanego zespołu (bądź też jednej lub dwóch osób w przypadku małego projektu), który jest odpowiedzialny za całą automatyzację testów. Zwykle wszyscy członkowie takiego zespołu piszą kod i odpowiadają za implementowanie skryptów testowych, infrastruktury oraz kodu wielokrotnego użytku, a także za utrzymywanie kodu, badanie wyników oraz usprawnianie systemu automatyzacji testów w miarę upływu czasu.

Dużą zaletą takiego zespołu jest to, że jego członkowie dzielą się między sobą wiedzą i doświadczeniem, a także wykorzystują ponownie kod bez żadnych ograniczeń. Jest to zwłaszcza istotne na początku, kiedy infrastruktura i praktyki są dopiero formowane. Ponadto, jeśli zespoły deweloperów podzielone są wzdłuż granic architektonicznych i technologicznych – np. „zespół klienta”, „zespół serwera”, „zespół bazy danych” itd. – wówczas bardziej sensowne wydaje się posiadanie dedykowanego zespołu automatyzacji, który zaimplementuje testy kompleksowe dla całego systemu. Więcej informacji na temat związków pomiędzy automatyzacją testów, architekturą i strukturą biznesu można znaleźć w rozdziałach 6 i 8.

Z drugiej strony, ponieważ taki zespół jest bardzo zwarty, zwykle nie będzie on blisko współpracował z innymi deweloperami. Jedną z konsekwencji tej sytuacji jest to, że zespół taki zwykle pisze testy w momencie, gdy testowane przez nie funkcje są już ukończone i w miarę stabilne (po tym, jak testerzy przynajmniej raz przetestują je ręcznie). Zazwyczaj otrzymują oni istniejące scenariusze testów utworzone wcześniej przez testerów manualnych i automatyzują je, ewentualnie dostosowując je po drodze pod automatyzację. Jednak ta separacja między deweloperami aplikacji i deweloperami automatyzacji rodzi pewne problemy:

1. Jeśli testowany kod nie był pisany pod kątem testowania, jego automatyzacja może być bardzo trudna. Aby to zmienić, deweloper aplikacji musiałby oderwać się od swojej bieżącej pracy i zmodyfikować projekt funkcji, którą wcześniej zaimplementował, a która została już nawet przetestowana ręcznie. Z tego powodu sytuacja taka będzie miała miejsce bardzo rzadko…

2. Podobne problemy mogą wystąpić, gdy znajdziemy błąd podczas implementowania automatycznego testu. Jeśli dana funkcja została już przetestowana ręcznie, znaleziony przez nas błąd może nie być krytyczny, ale może utrudnić właściwe zaimplementowanie automatyzacji. Tu również trzeba przerwać pracę dewelopera aplikacji, aby to naprawił, inaczej deweloper automatyzacji nie będzie mógł kontynuować pracy nad tym testem.

Kolejną wadą tego podejścia jest to, że ponieważ odpowiedzialność za badanie niepowodzeń spoczywa głównie na zespole automatyzacji, a nie na zespołach programistów, może nam być niezwykle trudno ustabilizować testy. Każda zmiana dokonywana przez deweloperów aplikacji może sprawić, że jeden lub więcej testów będzie kończyć się niepowodzeniem, a sami deweloperzy zwykle nie będą się tym przejmować, dopóki nie udowodnimy im, że dzieje się tak na skutek błędu. Więcej informacji na temat konsekwencji takich procesów biznesowych można znaleźć w rozdziale 5.

Dedykowany deweloper automatyzacji wewnątrz każdego zespołu

W organizacjach tworzących oprogramowanie, w których zespoły organizowane są wokół konkretnych funkcji zamiast według technologicznych lub architektonicznych granic, często bardziej sensowna wydaje się obecność jednego lub dwóch deweloperów automatyzacji w każdym takim zespole. Dotyczy to szczególnie sytuacji, gdy taka organizacja zamierza pokryć testami automatycznymi każdą nową funkcję (lub historyjkę użytkownika), zanim zostanie ona uznana za ukończoną.

W takim przypadku zaleca się, aby wszyscy deweloperzy automatyzacji mieli możliwość dzielenia się ze sobą wiedzą, kodem i pomysłami, zaś jeden starszy deweloper automatyzacji, nienależący do żadnego konkretnego zespołu, zapewniał profesjonalne wsparcie i nadzorował prace innych deweloperów automatyzacji

Oczywiście podejście to nie wpisuje się dobrze w sytuację, gdy automatyzujemy istniejące, starsze manualne przypadki testowe, ponieważ w takim wypadku nie będzie żadnej współpracy między deweloperem automatyzacji a deweloperem aplikacji. Jeśli pełne pokrycie nie zostało jeszcze osiągnięte, pomóc może obecność kilku deweloperów automatyzacji pracujących nad starszymi testami, podczas gdy pozostali deweloperzy będą pracować nad nowymi testami w zespołach tworzących funkcje. Więcej informacji na temat sposobów przybliżania się do pełnego pokrycia przy jednoczesnym zapewnieniu pokrycia nowych funkcji, można znaleźć w rozdziale 4.

W małych organizacjach lub zespołach może pracować jeden lub dwóch deweloperów automatyzacji, który współpracują z deweloperami aplikacji przy tworzeniu nowych funkcji, uzupełniając w pozostałym czasie wszelkie luki w testach regresji.

Największą zaletą tego podejścia jest łatwość utrzymania „zielonej” automatyzacji, ponieważ odpowiedzialność za dostarczanie nowych funkcji ze wszystkimi działającymi testami leży po stronie całego zespołu. Ponadto pisanie testów podczas opracowywania danej funkcji gwarantuje łatwość testowania aplikacji.

Dawanie deweloperom pełnej odpowiedzialności za automatyzację

Niektóre zespoły idą o krok dalej i decydują, że zamiast dedykowanego dewelopera automatyzacji w każdym zespole tworzącego funkcje, to deweloperzy aplikacji będą pisać i utrzymywać testy automatyzacji. Tradycyjnie deweloperzy dokonują tego za pomocą testów jednostkowych (patrz rozdział 17), ale nie ma żadnego powodu, aby nie mogli oni tego robić również za pomocą testów o szerszym zakresie. W rzeczywistości pierwsi orędownicy podejścia tworzenia oprogramowania sterowanego testami (Test-Driven Development, TDD) – Kent Beck i Martin Fowler – twierdzą, że terminu „testy jednostkowe” używają nie tylko dla testów poziomu pojedynczej klasy lub metody, ale w zasadzie dla testów o dowolnym zakresie¹¹.

Może to być bardzo dobre podejście, jeśli tylko wszyscy deweloperzy (lub przynajmniej kilku z nich z każdego zespołu) dysponują umiejętnościami wymaganymi do pisania dobrych testów. Tak jak jedni deweloperzy specjalizują się wyłącznie w rozwoju „klientów”, a inni wyłącznie w rozwoju „serwerów”, tak istnieją również deweloperzy wszechstronni, za każdy z nich może mieć umiejętności wymagane przy pisaniu dobrych testów lub ich nie mieć.

W małych organizacjach, które dysponują odpowiednimi osobami, może to działać bardzo dobrze. Jednak menedżerowi zespołu deweloperów w dużej organizacji nie poleciłabym tego podejścia ze wszystkimi jego aspektami, ponieważ nie wszystkie zespoły mogą mieć osoby o odpowiednich umiejętnościach. Taki menedżer powinien znaleźć eksperta (pracującego wewnątrz lub poza organizacją), który przeszkoli i będzie towarzyszył kolejno jednemu zespołowi po drugim, pomagając im przestawić się na ten sposób myślenia i pracy. Jest to istotne, ponieważ zwykle każdy zespół ma inne problemy i ograniczenia, tak więc podejście „uniwersalne” może być bardzo niebezpieczne i prowadzić do niskiej jakości testów, które nie są wiarygodne i są trudne w utrzymaniu. Dodatkowo ważne jest, aby promować przekazywanie wiedzy między zespołami, zarówno w celu tworzenia spójnych praktyk, jak i optymalizowania procesów pracy, głównie poprzez dokonywanie przeglądów kodu i programowanie w parach.

Różnorodność narzędzi

Jak już wspomnieliśmy wcześniej, wyboru odpowiedniego narzędzia należy dokonać dopiero po przeczytaniu wszystkich rozdziałów z części I tej książki. Ponieważ jednak wyjaśniliśmy już, że jednym z najbardziej istotnych czynników mających wpływ na wybór narzędzi jest sposób, w jaki ludzie będą ich używać, możemy przystąpić do przedstawienia różnego rodzaju dostępnych narzędzi. Zwróćmy uwagę, że w większości przypadków będziemy korzystać z kombinacji tych narzędzi, ponieważ różne narzędzia stanowią odpowiedź na różne problemy, a razem dostarczają kompletne rozwiązanie. W wielu przypadkach będziemy również musieli zbudować nasze własne małe narzędzia (głównie w celu połączenia ze sobą innych narzędzi) lub zostaniemy zmuszeni do korzystania z pewnych starszych narzędzi, które zostały wcześniej opracowane w naszej organizacji. W kolejnych punktach pogrupujemy te narzędzia w odrębne kategorie, w ramach których podamy kilka przykładów i omówimy czynniki przemawiające za ich wyborem.

Klasyfikacja narzędzi

Zanim przejdziemy dalej, musimy wyjaśnić kilka rzeczy. Po pierwsze, klasyfikacja ta nie jest ostateczna czy bezsporna, ponieważ wielu z tych narzędzi nie da się jednoznacznie zakwalifikować do pojedynczej kategorii. Inne osoby mogą sklasyfikować te narzędzia w inny sposób. Mimo że część z tych narzędzi jest do siebie podobna, każde z nich ma swoje niepowtarzalne funkcje oraz cechy. Ponadto wiele z tych narzędzi ma zastosowanie do więcej niż jednego problemu, dlatego mogą one należeć do więcej niż jednej kategorii. W ogólnym przypadku większość narzędzi, które nie wymagają od nas umiejętności programistycznych, oferuje więcej funkcji, podczas gdy narzędzia wymagające umiejętności kodowania są zwykle przeznaczone do bardziej konkretnego, węższego celu, ale możemy je w prosty sposób łączyć z innymi narzędziami.

Zwróćmy uwagę, że choć w każdej takiej kategorii podajemy przykłady najbardziej popularnych narzędzi, to w żadnym wypadku nie jest to wyczerpująca lista narzędzi w danej kategorii. Przykłady te zostały głównie oparte na doświadczeniu i wiedzy autora tej książki, tak więc nie są one w żaden sposób promowane względem pozostałych narzędzi. Narzędzia i technologie pojawiają się i znikają. Tym samym konkretne przykłady i opisy funkcji zapewne dosyć szybko staną się nieaktualne krótko po opublikowaniu tej książki. Jednak w ogólnym przypadku sama ta klasyfikacja, jak i przedstawione tu główne koncepcje, pozostaną niezmienione jeszcze przez długi czas.

IDE12 i języki programowania

Bez względu na to, czy zdecydujemy się tworzyć automatyzację poprzez pisanie kodu, czy też skorzystamy z jakiegoś narzędzia, które jest bardziej odpowiednie dla osób bez umiejętności programistycznych, deweloper automatyzacji będzie wykonywać większość swojej pracy w ramach pewnej aplikacji, która zapewnia główne środowisko pracy. Za pomocą tego narzędzia będzie on tworzył i utrzymywał testy, a zwykle także wszelkie inne artefakty będące częścią systemu automatyzacji testów.

Jeśli zdecydowaliśmy się skorzystać z narzędzi bardziej odpowiednich do osób niebędących programistami, to narzędzia te zwykle składać się będą ze swoich własnych specjalistycznych środowisk, których poznanie i wykorzystywanie będzie łatwiejsze dla takich osób. W takim wypadku nie mamy zwykle żadnego wyboru dotyczącego IDE, ponieważ będzie to po prostu ta sama aplikacja, która zapewnia technologię umożliwiającą tworzenie, edytowanie i uruchamianie automatyzacji testów. Zauważmy jednak, że zwykle narzędzia te nie tylko generują w uniwersalnym języku programowania kod, który deweloper automatyzacji może zmodyfikować, ale też często pozwalają programistom na rozszerzenie automatyzacji poprzez pisanie modułów z użyciem niestandardowego kodu. Do edycji tych plików niektóre z tych narzędzi dostarczają swoje własne środowiska, ale większość z nich umożliwia deweloperowi skorzystanie z zewnętrznego, powszechnie używanego (i skierowanego raczej do programistów) IDE.

Jeśli automatyzację planujemy napisać głównie w kodzie, to musimy również zdecydować się na konkretny język programowania. Choć wiele IDE pozwala nam pracować w wielu językach programowania, a kod wielu języków programowania można pisać w różnych IDE, to jednak większość języków ma swoje własne „naturalne” środowiska. Z tego powodu, gdy już zdecydowaliśmy się na konkretny język programowania, zwykle wybór zintegrowanego środowiska programowania jest już dość prosty.

Jeśli chodzi o wybór języka programowania, to należy wziąć pod uwagę kilka czynników. Po pierwsze, choć w większości języków programowania możemy robić praktycznie wszystko to, co chcemy, to niektóre narzędzia (przykładowo dedykowane automatyzacji interfejsu użytkownika) działają wyłącznie z określonym językiem programowania. Na przykład Coded UI firmy Microsoft działa wyłącznie z językami C# lub VB.NET. Nie możemy pisać testów Coded UI w Javie lub Pythonie. Jednak niektóre narzędzia, takie jak Selenium, są albo obsługiwane przez wiele różnych języków, albo mają swoje alternatywy w innych językach.

Jeśli nie ogranicza nas technologia i możemy wybierać spośród wielu różnych języków programowania, to powinniśmy wziąć pod uwagę poniższe czynniki:

Po pierwsze, jest wysoce zalecane, aby korzystać z tego samego języka programowania, którego używają pozostali deweloperzy w naszym zespole. Dla testów jednostkowych decyzja ta jest oczywista, ponieważ nie tylko jest to najprostszy sposób, ale też testy jednostkowe pisane są zwykle przez tych samych programistów, którzy tworzą kod systemu. Takie podejście zaleca się również w odniesieniu do pozostałych testów automatyzacji, głównie z powodu transferu wiedzy, współpracy oraz ponownego wykorzystywania wspólnych narzędzi przez deweloperami automatyzacji i deweloperami produktu. W niektórych firmach zdarzało się, że pojedynczy deweloper automatyzacji decydował się korzystać z innego języka niż pozostali członkowie zespołu (bo przykładowo był on z nim lepiej zaznajomiony), przez co później firma ta była zmuszona pozostać już przy tej decyzji i stosować różne obejścia, aby zintegrować ten język z systemem kompilacji lub innymi narzędziami, i to czasem już na długo po tym, jak ten deweloper automatyzacji opuścił tę firmę. Zmiana języka programowania w późniejszym etapie jest praktycznie niemożliwa!

Popularność – w większości przypadków lepiej jest wybrać popularny, dobrze ustabilizowany język niż jakiś język niszowy. Unikajmy wybierania „najnowszego i najfajniejszego” języka, z którym doświadczenie ma niewielka liczba osób (z takich samych powodów starajmy się nie wybierać żadnego przestarzałego języka). Istnieje ku temu kilka powodów:

Wybór popularnego języka ułatwi nam rekrutację dodatkowych deweloperów automatyzacji, gdy będzie to konieczne.

Łatwiej nam będzie znaleźć pomoc i samouczki w Internecie, a także wziąć udział w zajęciach wykładowych.

Będziemy mieli do dyspozycji więcej narzędzi i bibliotek.

W czasie pisania tej książki najpopularniejszymi językami programowania były Java, C#, Python i JavaScript. Istnieje również rozszerzenie dla języka JavaScript o nazwie TypeScript, które jest z nim w pełni kompatybilne, ale wprowadza do tego języka wiele dodatkowych funkcji.

Funkcje języka – choć w ogólnym przypadku w każdym języku programowania możemy napisać dowolny program, a większość języków oferuje podobny zestaw podstawowych konstrukcji (takich jak instrukcje „if”, zmienne, metody itd.), to każdy język ma swoje własne unikalne funkcje, jak również własne ograniczenia. Te funkcje i ograniczenia mogą mieć znaczący wpływ na czytelność, możliwość ponownego wykorzystywania i łatwość utrzymywania naszego kodu! Niektóre funkcje języka mogą być w nim implementowane kosztem innych korzyści, jakie oferowane są przez pozostałe języki. W szczególności większość języków zawiera funkcje, które pozwalają programistom ograniczyć popełniane przez nich błędy! Choć funkcje te czasem wprawiają w zakłopotanie młodszych programistów, to jednak pomagając nam popełniać mniejszą ilość błędów, pomagają zwiększyć niezawodność kodu i uczynić go bardziej solidnym. Przykłady takich funkcji języka podano poniżej.

Porównanie funkcji języka

Choć nie jest to wyczerpujące porównanie funkcji języków programowania, to powinno ono dać nam dobry pogląd na to, jakie funkcje są dostępne w różnych językach programowania i jakie przynoszą one korzyści. Zwróćmy uwagę, że „funkcje języka” nie są tym samym, co funkcje podstawowych bibliotek języka. Choć każdy język zwykle ma swój własny zestaw podstawowych bibliotek, które dostarczają pewne podstawie usługi, takie jak operacje matematyczne, listy, powszechne struktury danych, drukowanie, operacje na plikach, data i godzina itd., to funkcje języka są bardziej ogólnym konstruktem syntaktycznym rozpoznawanym przez kompilator, który możemy wykorzystać do ustrukturyzowania naszego kodu, bez względu na to, co ten kod tak naprawdę robi.

Silne typowanie kontra typowanie dynamiczne. Silne typowanie oznacza, że typ zmiennej lub parametru musi być jawnie zadeklarowany, tak aby kompilator mógł sprawdzić poprawność jego użycia już na etapie kompilacji. W niektórych językach możemy łączyć silne typowanie z typowaniem dynamicznym. Java obsługuje wyłącznie silne typowanie. C# jest głównie silnie typowanym językiem, ale obsługuje również typowanie dynamiczne (za pośrednictwem słowa kluczowego dynamic). Python i JavaScript obsługują jedynie typowanie dynamiczne, zaś TypeScript dodatkowo typowanie silne.

Enkapsulacja – Zdolność do kontroli zakresu dostępności zmiennej lub metody, zwykle poprzez zadeklarowanie członków klasy jako członków publicznych lub prywatnych. Wszystkie języki obiektowe, w tym Java i C#, obsługują tę funkcję. To samo dotyczy języka TypeScript. JavaScript pozwala to osiągnąć na swój własny sposób, który polega na deklarowaniu zagnieżdżonych funkcji, a następnie deklarowaniu zmiennych lokalnych w tych funkcjach wewnętrznych. Funkcja ta nie jest dostępna w Pythonie.

Polimorfizm lub funkcje wywołań zwrotnych – Choć polimorfizm jest uważany za typowy element świata obiektowego, a funkcje wywołań zwrotnych już nie, tak naprawdę oferują one mniej więcej takie same korzyści. Krótko mówiąc, umożliwiają one zmiennym na odwoływanie się nie tylko do danych, ale również do funkcjonalności, z możliwością przekazywania ich do metod oraz uzyskiwania ich z metod. Pozwala nam to w prosty sposób rozszerzyć zachowanie kodu bez konieczności modyfikowania jego podstawowej logiki. Wszystkie popularne języki oferują przynajmniej jedną z tych funkcji, jednak niektóre języki skryptowe, zwłaszcza te projektowane z myślą o konkretnym celu lub narzędziu, są tych funkcji pozbawione.

Wyrażenia lambda i domknięcia – Jest to zdolność do definiowania metody wewnątrz innej metody, a następnie odwoływania się do zmiennych lokalnych metody zewnętrznej z poziomu metody wewnętrznej. Wyrażenia lambda zwykle pozwalają nam osiągnąć to samo za pomocą krótszej i bardziej eleganckiej składni. Domknięcia są w pełni wspierane przez języki C#, JavaScript i TypeScript. Ponadto C#, TypeScript i JavaScript ES6 obsługują dodatkowo składnię wyrażeń lambda. Java i Python również obsługują składnię wyrażeń lambda, ale ich implementacje domknięć są w pewnym stopniu ograniczone.

Wielowątkowość – Zdolność do równoległego wykonywania kodu. Choć pisanie solidnego i niezawodnego kodu wielowątkowego jest bardzo trudne – przez co powinni go unikać nawet doświadczeni programiści, jeśli tylko mają jakiś inny wybór – to nadal możemy używać zewnętrznych bibliotek, które go wykorzystują. Wielowątkowość jest obsługiwana przez języki Java, C# i Python, ale nie przez JavaScript. Choć JavaScript oferuje pewne mechanizmy, które zapewniają współbieżność (w ramach koncepcji nazywanych „obietnicami” (promises), to nie mamy tu do czynienia z pełną wielowątkowością. Z tego powodu, jeśli korzystamy z Selenium w kodzie JavaScript (WebDriverJS), to nasz kod stanie się znacznie bardziej skomplikowany i problematyczny, a także trudniejszy w debugowaniu niż w przypadku innych języków. Słowa kluczowe async i await, dostępne są w językach TypeScript i JavaScript ES2017, stanowią pewne rozwiązane tego problemu, ale nadal kod Selenium w językach JavaScript i TypeScript nie jest tak czytelny i łatwy w debugowaniu jak w przypadku pozostałych języków.

Choć Python stał się w ostatnim czasie dosyć popularny w obszarze automatyzacji testów – prawdopodobnie z powodu jego uproszczonej składni i łatwości nauki, co pozwala na jego szybkie przyswojenie przez osoby niebędące programistami – to zwykle nie polecam stosowania tego języka w automatyzacji testów z powodu wspominanych wyżej ograniczeń, przy czym inne czynniki opisane powyżej mogą przechylić szalę na jego korzyść. Moim własnym ulubionym językiem programowania jest C#, dlatego też używam go w przykładach prezentowanych w części II tej książki, ale przyznaję, że jest to głównie kwestia przyzwyczajenia…

Biblioteki testowania (jednostkowego)

Jeśli piszemy nasze testy za pomocą kodu, potrzebujemy narzędzia, które pozwolą nam je uruchamiać. Co prawda można by napisać wszystkie testy w formie jednego prostego programu wiersza polecenia, który wykonuje je sekwencyjnie i wyświetla ich wyniki, jednak znacznie łatwiej będzie nam pisać i uruchamiać indywidualne testy z użyciem jakiejś biblioteki testowania. Wtedy, z poziomu wiersza polecenia lub narzędzia z interfejsem graficznym, biblioteka taka pozwoli nam podejrzeć listę dostępnych testów, uruchomić je i zobaczyć, które z nich zakończyły się sukcesem, a które niepowodzeniem. Zwykle możemy zdecydować, które z tych testów chcemy uruchomić – wszystkie lub tylko wybrane testy, lub też wyłącznie testy, które mają określone cechy.

Ponadto narzędzia te umożliwiają nam definiowanie specjalnych metod, które są wykonywane przed oraz po każdym teście, przed i po grupie testów, a także przed i po wszystkich testach, co pozwala zagwarantować, że testy nie będą ze sobą kolidowały. Niektóre biblioteki pozwalają nam również określać zależności między testami, mimo że płynąca z tego korzyść jest wątpliwa, ponieważ w ten sposób testy służą dwóm różnym celom – inicjalizowaniu i testowaniu, które nie idą ze sobą w parze i utrudniają ich utrzymywanie. Dodatkowo uniemożliwia to równoległe uruchamianie zależnych testów.

Biblioteki testowania są zwykle projektowane pod kątem testów jednostkowych, ale nadają się one równie dobrze do testów integracyjnych i testów systemu. Nie panikujmy więc, gdy widzimy termin „biblioteka testów jednostkowych”, jaki jest zwykle stosowany do opisu tych narzędzi. Przykładami takich bibliotek są chociażby: JUnit i TestNG dla Javy i MSTest, NUnit i xUnit dla .NET. Dla języka Python mamy wbudowane biblioteki unittest oraz py.test, zaś w przypadku języka JavaScript najbardziej popularne są Jasmine i Mocha.

Wszystkie popularne biblioteki testów jednostkowych są albo częścią jakiegoś środowiska IDE, częścią jakiegoś zestawu narzędzi dla danego języka, albo też darmowymi projektami open source. Tym samym ich cena nie powinna stanowić dla nas problemu…

Uwaga

Biblioteki testowania nazywane są czasem jarzmem testowym (test harness).

Biblioteki asercji

W większości takich bibliotek uznaje się, że test zakończył się powodzeniem, jeśli nie zgłasza on żadnego wyjątku (tj. podczas jego wykonywania nie wystąpił żaden błąd). Dobrą praktyką jest jednak wykonanie jakiejś weryfikacji na końcu każdego testu, zwykle poprzez porównanie faktycznego rezultatu testowanej operacji z oczekiwanym wynikiem. Z tego powodu większość bibliotek testowania pozwala nam na wykonywanie takich weryfikacji przy użyciu prostego mechanizmu nazywanego asercjami (assertions). Typowa asercja pozwala nam porównać uzyskany rezultat z rezultatem oczekiwanym. Jeśli obie te wartości różnią się od siebie, to zgłaszany jest odpowiedni wyjątek i efekcie cały test kończy się niepowodzeniem. Choć większość bibliotek testowania ma własne metodami asercji, to istnieją dedykowane biblioteki asercji, które oferują dodatkowe korzyści. Niektóre z tych bibliotek dostarczają bardziej szczegółowe metody asercji, przykładowo do sprawdzania poprawności komunikatów odpowiedzi HTTP. Inne są bardziej elastyczne i pozwalają nam definiować nasze własne asercje, zwykle w bardzo czytelny i „płynny” sposób.

Uwaga

Wiele bibliotek testowania, w tym również bibliotek dostawców zewnętrznych, oferuje mechanizmy do tworzenia atrap dla obiektów¹³. Ponieważ jednak mechanizmy te przydają się wyłącznie do czystych testów jednostkowych, nie będziemy ich tutaj omawiać. Więcej informacji na ich temat można znaleźć w rozdziale 17.

Biblioteki w stylu BDD

Tworzenie oprogramowania sterowane zachowaniem (Behavior-Driven Development, BDD) jest metodyką, która wywodzi z TDD (Test Driven Development) (patrz rozdział 17), ale kładzie większy nacisk na zmniejszanie luki pomiędzy opisem zachowania jakiejś funkcji (znajdującym się w formalnych specyfikacjach) w języku naturalnym a testami, które weryfikują, czy dana funkcja faktycznie zachowuje się zgodnie z tym opisem. Z tego powodu opisy te nazywane są przez niektórych wykonywalnymi specyfikacjami lub żywą dokumentacją. Kolejnym aspektem tej metodyki jest to, że testy wykorzystywane są jako kryteria akceptacji dla historyjki użytkownika, dlatego jest ona również nazywana tworzeniem oprogramowania sterowanym testami akceptacyjnymi (Acceptance Test-Driven Development, ATDD). Metodyka ta jest omawiana szczegółowo w rozdziale 16.

Zastosowanie metodyki BDD sprowadza się do użycia narzędzi, które pozwalają nam pisać testy za pomocą zdań w języku naturalnym i powiązać każde takie zdanie z metodą, która wykonuje operacje opisywane przez to zdanie. Zdania ta, wraz odpowiadającymi im metodami implementacji, mogą być wykorzystywane ponownie, dzięki czemu dokumentacja i testy są bardziej spójne.

Krótko mówiąc, narzędzia obsługujące BDD pozwalają nam tłumaczyć czytelne dla człowieka specyfikacje na wykonywalny kod. Najbardziej popularnym narzędziem BDD jest Cucumber, opracowany pierwotnie dla języka Ruby, a później przeniesiony do innych języków, w tym do Javy i C# (gdzie występuje on pod nazwą SpecFlow). Cucumber wykorzystuje specjalny język o nazwie Gherkin, składający się z bardzo niewielu słów kluczowych, po których wprowadzane są zdania w języku naturalnym.

Poniżej znajduje się przykład scenariusza napisanego w języku Gherkin:

Scenario: Cash withdrawal charges commission

Given the commission for cash withdrawal is $1

And I have a bank account with balance of $50

When I withdraw $30

Then the ATM should push out $30

And the new balance should be $19

And the charged commission should be $1

W powyższym przykładzie wytłuszczone słowa (Scenario, Given, And, When oraz Then) są słowami kluczowymi języka Gherkin, zaś pozostały tekst jest w języku naturalnym. Metody zostają powiązane z tymi zdaniami za pomocą wyrażeń regularnych, co pozwala nam określać dodatkowe parametry, jak choćby wartości wyrażające ilości, które w powyższym przykładzie zostały oznaczone kursywą.

Większość bibliotek BDD generuje w tle kod dla szkieletu testu jednostkowego, wykorzystując przy tym jedną z istniejących bibliotek testów jednostkowych oraz jeden z popularnych języków programowania. Wygenerowany w ten sposób szkielet testu jednostkowego wywołuje pewne metody, które musimy zaimplementować, a każda taka metoda powiązana jest zwykle z jednym zdaniem w języku naturalnym w scenariuszu Gherkin w celu wykonania rzeczywistej pracy.

Kolejnym popularnym narzędziem z tej kategorii jest Robot Framework. Choć Robot Framework również obsługuje język Gherkin, to posługiwanie się w nim tym językiem nie jest wymagane. Robot Framework dostarczany jest z pewnymi wbudowanymi bibliotekami do wykonywania typowych akcji, operacji czy walidacji, a dodatkowo zawiera większy zestaw bibliotek zewnętrznych, zapewniających różnorodne sposoby oddziaływania z testowanym systemem (zobacz kolejny podrozdział). Oczywiście możemy również napisać nasze własne biblioteki za pomocą języków Python lub Java.

W niektórych z tych narzędzi przyjęto nieco inne podejście, aby dostarczyć nam możliwość zawierania dokumentacji w obrębie samego kodu testu. Przykładami takich narzędzi są RSpec dla języka Ruby, Spectrum dla Javy, MSpec dla .NET oraz Jasmin i Mocha dla języka JavaScript, które są również bibliotekami testowania.

Technologie zapewniające interakcję z testowanym systemem

Bez względu na to, czy piszemy testy bezpośrednio w kodzie, czy za pomocą jakiegoś innego narzędzia, test musi w jakiś sposób komunikować się z testowanym systemem. Najbardziej oczywistym sposobem osiągnięcia tego jest zasymulowanie pracy użytkownika w interfejsie systemu. Nie zawsze jednak jest to najlepsze rozwiązanie (więcej informacji o wadach i zaletach testowania za pośrednictwem interfejsu użytkownika można znaleźć w rozdziale 6). Czasem lepsza może okazać się interakcja z testowanym systemem za pomocą protokołu HTTP, TCP/IP lub jakiegoś innego protokołu komunikacji, za pośrednictwem bazy danych, poprzez tworzenie, modyfikowanie lub odczytywanie z plików wykorzystywanych przez testowany system, poprzez uruchamianie komend z wiersza polecenia itd. Większość z tych działań możemy wykonywać z poziomu kodu przy użyciu standardowych interfejsów programowania aplikacji (API) i bibliotek.

Jednak większość technologii interfejsu użytkownika nie dostarcza prostego w użyciu API do symulowania czynności wykonywanych przez użytkownika, ponieważ interfejs użytkownika powinien być wykorzystywany przez użytkownika, a nie przez inną aplikację… Czasem technologie te dostarczają takie API, ale są to zwykle niskopoziomowe interfejsy, których bezpośrednie wykorzystywanie w testach automatycznych nie jest wcale takie proste. Do symulowania działań użytkownika wykonywanych za pośrednictwem interfejsu użytkownika zwykle potrzebne jest dedykowane narzędzie, które pozwoli na zautomatyzowanie interfejsu użytkownika dla konkretnej technologii. Najbardziej znanym przykładem takiego narzędzia jest Selenium, które automatyzuje interfejsy aplikacji sieci Web.

Jeśli planujemy wchodzić w interakcję z testowanym systemem za pomocą protokołu HTTP i zamierzamy napisać automatyzację w kodzie, to możemy po prostu napisać nasz własny kod, który będzie wysyłał żądania i będzie przetwarzał odpowiedzi jak każdy inny klient. W ten sposób zapewnimy sobie maksymalną elastyczność, a przy okazji dowiemy się, co jest potrzebne do napisania takiej aplikacji klienta. Z uwagi na to, że protokół HTTP jest bardzo popularnym sposobem interakcji z testowanym systemem, powstało kilka narzędzi i bibliotek, których celem jest ułatwienie nam tej pracy. Niektóre z tych narzędzi były przeznaczone do użycia z poziomu kodu (np. RestAssured dla Javy), a niektóre są samodzielnymi narzędziami (np. SoapUI firmy SmartBear). Istnieją także narzędzia, których głównym zadaniem jest ułatwianie nam wysyłania i/lub monitorowania żądań oraz podglądanie uzyskiwanych odpowiedzi za pośrednictwem interfejsu użytkownika, dając nam jednocześnie możliwość tworzenia makr lub testów automatycznych. Ponieważ jednak automatyzacja testów nie jest ich głównym celem, zwykle nie stanowią one najlepszego wyboru dla kompletnego systemu automatyzacji testów. Przykładami takich narzędzi są Fiddler i Postman.

INTERFEJS PROGRAMOWANIA APLIKACJI (API)

Podczas gdy większość aplikacji projektuje się tak, aby były one kontrolowane przez użytkowników za pośrednictwem interfejsu użytkownika, niektóre aplikacje i komponenty oprogramowania są projektowane w taki sposób, aby były kontrolowane przez inne aplikacje (lub komponenty oprogramowania). Ponadto wiele aplikacji może być kontrolowanych zarówno przez użytkowników, jak i przez inne programy. Aby jakaś aplikacja mogła być kontrolowana przez inne aplikacje, powinna udostępniać interfejs programowania aplikacji (Application Programming Interface, API), tak aby inne programy, będące klientami lub konsumentami takiego API, mogły za jego pomocą ją kontrolować. Z technicznego punktu widzenia API mogą być dostarczane w wielu różnych formach lub kształtach, ale od strony pojęciowej wszystkie API definiują zbiór operacji, które klient może wywoływać, wraz z odpowiadającymi im parametrami, strukturami danych, wynikami itd. API powinny być zwykle dobrze udokumentowane, aby deweloperzy aplikacji mogli z nich łatwo korzystać i wiedzieli, czego można oczekiwać od każdej takiej operacji. Technologie pozwalające aplikacjom na udostępnianie własnego API można podzielić na trzy grupy:

1. Bezpośrednie wywołania metod – aplikacja (lub częściej komponent oprogramowania) dostarcza zestaw metod (i klas, w większości nowoczesnych technologii), które aplikacja klienta może bezpośrednio wywoływać w obrębie tego samego procesu, podobnie jak klient wywołuje swoje własne metody.

2. Protokół komunikacji sieciowej – aplikacja definiuje zestaw komunikatów, które może wymieniać z klientem, a także ich dokładny format. Aplikacja udostępniająca API zwykle działa jako osobny proces, często na osobnej maszynie, i może obsługiwać wielu klientów jednocześnie. Obecnie najbardziej powszechnie wykorzystywanym protokołem podstawowym, za pomocą którego aplikacje udostępniają swoje API, jest HTTP (lub bardziej precyzyjnie HTTPS). API te zwykle definiują format komunikatów wykorzystywanych dla żądań i odpowiedzi, zgodnie ze stylem architektonicznym o nazwie REST (skrót od Representational State Transfer). Ponadto zwykle wykorzystują one notację JSON (JavaScript Object Notation) jako składnię dla formatów struktur danych i komunikatów. Nieco starszym stylem dla API HTTP, który nadal jest dosyć popularny, jest SOAP (Simple Object Access Protocol), oparty na języku XML (Extensible Markup Language).

3. Zdalne wywoływanie procedur (Remote Procedure Call, RPC) – ten typ technologii jest w zasadzie kombinacją dwóch poprzednich. Dzięki RPC operacje udostępniane poprzez API aplikacji są definiowane jako zestaw metod (procedur) i klas, podobnie jak ma to miejsce przy bezpośrednim wywoływaniu metod. Jednak w przeciwieństwie do bezpośredniego wywoływania metod, RPC jest wykorzystywane do wywoływania tych metod z klientów zdalnych, w innych procesach i maszynach. Wewnętrzna technologia RPC generuje metody zastępcze, które klient może wykorzystywać lokalnie. Metody te mają taka samą sygnaturę (nazwy metod i parametry) jak metody na serwerze, który udostępnia interfejs API. Te metody zastępcze szeregują nazwę (lub inny identyfikator) metody wraz z wartościami jej argumentów do postaci komunikatu i wysyłają go do serwera za pośrednictwem protokołu komunikacji sieciowej (np. HTTP). Następnie serwer analizuje składnię komunikatu i wywołuje odpowiednią metodę wraz z jej argumentami. Zgodnie ze stylem RPC może być na przykład wykorzystywana technologia Windows Communication Foundation (WCF). Z kolei Google oferuje technologię gRPC, zaś wiele usług udostępniających API REST dostarcza również powiązanie z językiem dla popularnych języków programowania, będące czymś w rodzaju wywołań RPC wyłącznie po stronie klienta.

Te trzy kategorie są jedynie kategoriami głównymi. Aplikacja może udostępniać API również w inny, mniej standardowy sposób, na przykład poprzez odczytywanie i zapisywanie ze współdzielonego pliku, z bazy danych, lub za pomocą jakiegoś sposobu komunikacji z aplikacjami.

Interfejsy API mogą być wykorzystywane w różnych celach:

1. Systemy operacyjne udostępniają bogaty zestaw API dla hostowanych w nich aplikacji. Interfejsy te mogą być używane do pracy z plikami, procesami, sprzętem, interfejsem użytkownika itd.

2. Komponenty programowe lub biblioteki wielokrotnego użytku udostępniają interfejsy API, z których mogą korzystać aplikacje. Zwykle taki interfejs jest jedynym sposobem wykorzystywania tych bibliotek. Biblioteki te mogą być przykładowo używane do wykonywania skomplikowanych operacji matematycznych lub do sterowania konkretnymi urządzeniami.

3. Wtyczki (plug-ins) – niektóre aplikacje mogą być rozszerzane przez zewnętrznych dostawców oprogramowania za pomocą wtyczek, w celu dostarczenia dodatkowych funkcji dla tej aplikacji lub zintegrowania jej z innymi aplikacjami. Na przykład edytor tekstowy może udostępniać API, które może być wykorzystywane przez wtyczki w różnych celach, takich jak sprawdzanie pisowni, integracja z systemami kontroli wersji, integracja z aplikacjami poczty e-mail itd. Czasem to samo API może być używane przez użytkowników do tworzenia makr, jak ma to miejsce w przypadku aplikacji pakietu Microsoft Office.

4. Usługi sieci Web udostępniają API (zwykle API REST), aby umożliwić innym aplikacjom ich wykorzystywanie. Na przykład witryna z prognozą pogody może udostępniać API, które może być wykorzystywane przez dostawców aplikacji w celu zintegrowania jej z tą usługą.

Narzędzia do nagrywania, edytowania i odtwarzania kontra biblioteki kodu

Generalnie narzędzia do automatyzacji interfejsu użytkownika można sklasyfikować albo jako narzędzia do „nagrywania i odtwarzania”, albo też jako zwykłe biblioteki, które możemy wykorzystywać w naszym własnym kodzie. Jednak w rzeczywistości mamy tu czynienia raczej z kontinuum, którego nie da się go jednoznacznie sprowadzić do dwóch osobnych kategorii. Z jednej strony tego kontinuum znajdziemy bardzo „głupie” narzędzia, które nagrywają ruchy i kliknięcia myszą lub wciśnięcia klawiszy, zapisują je i pozwalają nam je odtwarzać ponownie. Starsze osoby mogą pamiętać narzędzie Macro Recorder dostarczane swego czasu w systemie Windows 3.1… Na szczęście narzędzie to nie jest już częścią systemu Windows, a podobne programy nie są już tak popularne. Nie trzeba dodawać, że takie naiwne narzędzia są wysoce podatne na błędy, ponieważ na ślepo odtwarzają one akcje myszy i klawiatury, nie mając żadnej wiedzy o tym, czy coś się poruszyło, zmieniło itd.

Po drugiej stronie tego spektrum system operacyjny dostarcza niskopoziomowe API, które pozwalają nam odpytywać istniejące elementy (lub nawet piksele) wyświetlane w ramach interfejsu użytkownika i wysyłać do nich komunikaty, jak gdyby były one wysyłane przez mysz i klawiaturę.

Jednak między tymi dwoma rodzajami narzędzi istnieje jeszcze wiele innych: przede wszystkim większość narzędzi rejestrujących kliknięcia myszą nie rejestruje jedynie współrzędnych X i Y tych kliknięć lub ruchów, ale starają się one raczej identyfikować elementy interfejsu użytkownika za pomocą jednej lub więcej ich właściwości, najlepiej jakiegoś unikalnego identyfikatora. Ponadto generują one kod w popularnym języku programowania, który można potem edytować, dostosować i utrzymywać według własnych potrzeb, lub też generują bardziej ogólny skrypt przeznaczony raczej dla osób bez doświadczenia programistycznego, który można edytować za pomocą dedykowanego edytora.

Zwróćmy uwagę, że prawie wszystkie narzędzia do automatyzacji interfejsu użytkownika są albo dostarczane z narzędziem, które pozwala nam badać elementy w interfejsie użytkownika naszej aplikacji (w tym również ich właściwości), albo są projektowane pod kątem ich wykorzystania w połączeniu z innym istniejącym narzędziem, dostarczanym zwykle w ramach pakietu SDK odpowiedniego systemu operacyjnego, co pozwala nam osiągnąć ten sam cel. Poniżej znajdują się opisy niektórych popularnych narzędzi do automatyzacji interfejsu użytkownika.

Selenium

Jest to prawdopodobnie najbardziej popularne narzędzie automatyzacji testów, które jest wykorzystywane głównie do automatyzacji interfejsu użytkownika w aplikacjach sieci Web. Jak wszystkie narzędzia do automatyzacji interfejsu użytkownika, Selenium pozwala nam wykonywać w imieniu użytkownika akcje myszy i klawiatury oraz pobierać wyświetlane dane. Selenium ma kilka istotnych zalet, które sprawiają, że narzędzie to jest tak bardzo popularne:

Jest narzędziem open source (dzięki czemu jest darmowy).

Obsługuje wiele różnych przeglądarek internetowych.

Jest dostępny w wielu językach programowania.

Podstawową wadą narzędzia Selenium jest to, że było ono projektowane z myślą o przeglądarkach, przez co obsługa innych technologii interfejsu użytkownika jest w nim mocno ograniczona i dostępna za pośrednictwem zewnętrznych rozszerzeń. Ponadto narzędzie to zostało zaprojektowane w taki sposób, aby było wykorzystywane głównie z poziomu kodu.

Aby zapewnić uniwersalność przeglądarek oraz języków programowania, narzędzie Selenium składa się z dwóch części, które możemy swobodnie wymieniać. Są to:

1. Powiązanie z językiem.

2. Sterownik przeglądarki.

Powiązanie z językiem jest biblioteką kodu, która dostarcza klasy i metody, z jakich możemy korzystać w kodzie naszych testów. Biblioteki te są albo skompilowane, jak w przypadku języków Java lub C#, albo są bibliotekami w czystym kodzie źródłowym, jak w przypadku języków Python lub JavaScript. Dla każdego obsługiwanego języka programowania stosuje się inne powiązanie z językiem¹⁴. Ta część komunikuje się ze sterownikiem przeglądarki za pomocą dedykowanego protokołu łączącego JSON.

Sterownik przeglądarki otrzymuje żądania od powiązania z językiem i wywołuje odpowiednie operacje w przeglądarce. Każdy typ przeglądarki ma swój własny sterownik. Ponieważ jednak wszystkie sterowniki „rozumieją” ten sam protokół łączący JSON, ten sam test może zostać użyty z innym sterownikiem i odpowiadającą mu przeglądarką.

Uwaga

Mimo że powiązanie z językiem komunikuje się ze sterownikiem za pośrednictwem protokołu HTTP, komunikacja ta nie ma żadnego związku z komunikacją między przeglądarką a serwerem aplikacji sieci Web.

Komponent powiązania z językiem dostępny w narzędziu Selenium nie jest biblioteką testowania, ale raczej zwykłą biblioteką kodu. Z tego powodu możemy używać go z dowolnego rodzaju aplikacji, mimo że jest on zwykle wykorzystywany z poziomu jakiejś biblioteki testów jednostkowych.

Elastyczna architektura narzędzia Selenium pozwala na zintegrowanie go z innymi specjalistycznymi narzędziami, łącznie z Selenium Grid, które umożliwia lokalne testowanie w różnych przeglądarkach, jak również z różnymi dostawcami testowania opartego na chmurze, takimi jak BrowserStack i SauceLabs. Zalety tej elastycznej architektury wykorzystuje dodatkowo narzędzie Appium, które umożliwia testowanie mobilne za pomocą API narzędzia Selenium.

Na rysunku 3.1 pokazano typową architekturę testu z użyciem Selenium.

Rysunek 3.1. Typowa architektura automatyzacji testów z użyciem Selenium

CZYM JEST SELENIUM WEBDRIVER?

Bez wdawania się w szczegóły, Selenium 1.0, nazywane również Selenium RC (lub Selenium Remote Control), było oryginalną technologią do automatyzowania interfejsu użytkownika w sieci Web. W wersji 2.0 narzędzie to zostało połączone z inną technologią o nazwie WebDriver, tworząc wspólnie narzędzie „Selenium WebDriver” będące popularną technologią, szeroko wykorzystywaną w ostatnich latach. Zwróćmy uwagę, że terminy „Selenium” oraz „WebDriver” często używane są zamiennie.

Jak wspomnieliśmy wcześniej, narzędzia do automatyzacji interfejsu użytkownika zwykle dostarczane są z narzędziem inspekcji do identyfikowania elementów interfejsu i ich właściwości. Selenium nie oferuje takiego narzędzia, ponieważ jest ono wbudowane we wszystkie nowoczesne przeglądarki internetowe. Wszystkie nowsze przeglądarki mają wbudowane narzędzia dla deweloperów (przeważnie otwierane klawiszem F12). Narzędzia te zawierają zwykle eksplorator DOM¹⁵, który umożliwia nam identyfikowanie elementów i ich właściwości. Rysunek 3.2 pokazuje eksplorator DOM w przeglądarce Chrome.

Rysunek 3.2. Eksplorator DOM w przeglądarce Chrome

Selenium IDE

Dalsza część książki dostępna w wersji pełnej

Przypisy

1 Tłumaczenie treści manifestu pochodzi ze strony: http://agilemanifesto.org/iso/pl/manifesto.html (przyp. tłum.)

2 Scrum jest najbardziej powszechną metodyką, która bazuje na wartościach zwinnego programowania.

3 Martin Fowler, „Refactoring: Improving the Design of Existing Code” (Addison-Wesley Professional, 1999).

4 Zintegrowane środowisko programowania (Integrated Development Environment, IDE) odnosi się do oprogramowania, które składa się głównie z edytora, kompilatora oraz zintegrowanego debugera. Przykładem najpopularniejszych środowisk dla języków C# i Java są Microsoft Visual Studio, Eclipse oraz IntelliJ.

5 Testy regresji są testami sprawdzającymi, czy funkcjonalność, która działała wcześniej zgodnie z założeniami, nadal działa poprawnie.

6 W tym kontekście „kod” odnosi się do dowolnego artefaktu będącego częścią systemu i mogącego wpływać na jego zachowanie. Jeśli na przykład lista państw w bazie danych jest czymś, czego użytkownik nie może i nie powinien zmieniać, to listę taką można uznać za część kodu aplikacji.

7 Operacja ewidencjonowania (check-in), znana również jako operacja commit, push lub submit, polega na zastosowaniu zmian wprowadzonych przez programistę na jego lokalnym komputerze w scentralizowanym repozytorium kodu źródłowego, który jest współdzielony przez cały zespół tworzący oprogramowanie. Repozytoria te zarządzane są przez systemy kontroli kodu źródłowego, takie jak Git, Microsoft Team Foundation Server, SVN, Mercurial i wiele innych.

8 Pewne wskazówki na temat dzielenia dużych historyjek użytkownika można znaleźć pod adresem http://agileforall.com/new-story-splitting-resource/.

9 Termin przypadek testowy (test case) może mieć wiele znaczeń. Dla mnie przypadek testowy jest jednym scenariuszem testowym, złożonym z konkretnych kroków (czynności) i weryfikacji. Przypadki testowe są zwykle grupowane w pakiety testów (test suites). Z kolei plan testowania zawiera zwykle wiele pakietów testów, w tym również inne szczegóły dotyczące planowania, zasobów itd.

10 Termin „skrypt” został tutaj użyty w celu opisania pojedynczego automatycznego przypadku testowego, bez względu na to, czy został on napisany w kodzie, w języku skryptowym, za pomocą narzędzia do nagrywania i odtwarzania, czy w dowolnej innej formie.

11 Zobacz opis testów „towarzyskich” (sociable tests) pod adresem https://martinfowler.com/bliki/UnitTest.html. Inne istotne odnośniki to: https://martinfowler.com/articles/is-tdd-dead/ oraz http://www.se-radio.net/2010/09/episode-167-the-history-of-junit-and-the-future-of-testing-with-kent-beck/ (od około 22 do 26 minuty).

12 IDE jest skrótem od Integrated Development Environment (Zintegrowane środowisko programowania). Są to aplikacje, która umożliwiają programistom pisanie, edytowanie, kompilowanie i debugowanie kodu, pozwalając przy tym na wykonywanie wielu innych czynności związanych z tworzeniem oprogramowania.

13 Atrapy nazywane są również obiektami imitacji (przyp. tłum.).

14 Niektóre języki kompilowane są do kodu pośredniego (byte-code), który wykonywany jest przez dedykowany silnik wykonawczy. Maszyna wirtualna Javy (JVM) oraz środowisko uruchomieniowe .NET (CLR) to najbardziej znane silniki wykonawcze kodu pośredniego. Biblioteki, które kompilowane są dla tych silników mogą być wykorzystywane przez aplikacje pisane w dowolnym języku, jaki może być skompilowany dla tego samego silnika. Przykładowo biblioteka WebDriver dla języka Java może być wykorzystywana przez testy pisane w językach Scala i Groovy, zaś powiązanie języka C# (.NET) – przez testy napisane w językach VB.NET oraz F#.

15 DOM to skrót od Document Object Model. DOM opisuje drzewo elementów HTML wraz z ich właściwościami, które w danej chwili są dostępne. Za pomocą języka JavaScript strona może manipulować swoim modelem DOM w czasie wykonywania, dzięki czemu strona jest dynamiczna. Zwróćmy uwagę, że choć DOM może się zmieniać, to sam kod HTML jest statycznym opisem strony, jaką serwer wysłał do przeglądarki.