Oferty w Twoim regionie
Włącz powiadomienia na pulpicie
Wróć na początek

Jakie powinny być testy automatyczne w Continuous Integration i jak z nich efektywnie korzystać

Continuous Integration to sposób tworzenia i rozwijania oprogramowania. Charakteryzuje się tym, że integracja nowego, pisanego przez programistów kodu do głównej gałęzi repozytorium następuje nawet kilka razy dziennie. Dzięki temu jednego dnia możemy mieć nawet kilka wersji aplikacji zbudowanych i zainstalowanych na środowisku testowym. Nadążenie za tak częstymi zmianami to dla utrzymującego testy automatyczne zespołu QA nie lada wyzwanie.

W niniejszym artykule przedstawię, jakie są – moim zdaniem – najważniejsze cechy testów automatycznych, dzięki którym osiągnięcie celu staje się prostsze, oraz wyjaśnię, jak wiedzę tę wykorzystywać w praktyce. Wszystkie moje rady są oparte na wieloletnim doświadczeniu w pracy nad testami automatycznymi w środowisku CI w różnych technologiach.

Poniższy tekst jest skierowany głównie do osób, które tworzą albo współtworzą rozwiązania testów automatycznych lub też planują zmierzyć się z takim zadaniem.

Przydatność testów automatycznych w ciągłej integracji

Żeby testy automatyczne były przydatne, w Continuous Integration muszą szybko dostarczać wiarygodnych informacji o jakości aplikacji. Jedno spojrzenie na wyniki powinno dawać odpowiedź na pytanie, czy w kolejnej wersji pojawiły się błędy regresyjne, czy też nie. Pomiędzy kolejnymi wersjami nie ma czasu na analizowanie testów, które zakończyły się niepowodzeniem z niewiadomych przyczyn.

Ponadto każda wprowadzona przez programistów zmiana w funkcjonalności to potencjalnie konieczność dopasowania testów automatycznych. Podobnie jak uruchamianie i analiza wyników, powinno to zabrać jak najmniej czasu, aby nie opóźniać dostarczenia informacji o jakości kolejnej wersji aplikacji.  

Większe zmiany funkcjonalne w aplikacji mogą pociągnąć za sobą konieczność dopisania kolejnych testów, które sprawdzą nowe obszary aplikacji,  takie jak wysyłanie przez aplikację e-maili, smsów, czy drukowanych dokumentów pdf. Testy automatyczne powinny być na tyle elastyczne, aby dodanie nowych modułów było szybkie i bezbolesne. Tylko jeśli powyższe warunki zostaną spełnione, będziemy w stanie szybko podać informację o jakości każdej kolejnej wersji aplikacji.

Co z tego wynika dla testów automatycznych

Powyższe wymagania można sprowadzić do trzech cech, którymi testy automatyczne powinny się charakteryzować: szybkość, łatwość utrzymania i stabilność. Na cechy te wpływa bardzo wiele czynników. Poniżej przedstawię te, które moim zdaniem, mają największe znaczenie dla osiągniecia celu.

Z rozmów z inżynierami testów pracującymi w różnych projektach wiem, że czynniki te często nie są wcale brane pod uwagę. Przyczynami tego stanu mogą być brak wiedzy, ograniczenia techniczne lub brak przekonania co do przydatności testów. Właśnie te obserwacje skłoniły mnie do przygotowania prezentacji i napisania niniejszego artykułu.

Szybkość testów

Żeby zdążyć z uruchomieniem całego zestawu testów automatycznych między kolejnymi wydaniami aplikacji, nasze testy muszą być albo bardzo szybkie, albo musimy znaleźć sposób na zagięcie czasoprzestrzeni i rozciągnięcie czasu, który mamy na uruchamianie testów. Ponieważ druga opcja nie jest zbyt realna, skupmy się na szybkości testów.

Dobre praktyki

Przyspieszanie testów to przede wszystkim stosowanie dobrych praktyk automatyzacji. Poniżej przedstawiam subiektywną listę działań, które najbardziej przyczyniają się do podniesienia szybkości wykonania testów.

Nie należy używać bezwzględnego zatrzymania testu

Bardzo złą praktyką jest stosowanie w testach automatycznych poleceń typu wait lub sleep, które zatrzymują wykonanie testu na ściśle określony czas. Doskonale rozumiem pokusę takiego rozwiązywania problemów ze stabilnością – niegdyś sam działałem podobnie.

Jest to jednak działanie krótkowzroczne. Po pierwsze, postępując w ten sposób opóźniamy niepotrzebnie  wykonanie testu o cenne sekundy (jeżeli plik ściągnie się w 5 sekund, marnujemy kolejne 5). Po drugie, mogą wystąpić okoliczności, w których nawet 10 sekund nie wystarczy i test, nie znalazłszy pliku, zgłosi błąd, mimo że plik zostanie ściągnięty w 11. sekundzie.

Zamiast bezwzględnego zatrzymania należy więc używać dynamicznego oczekiwania na konkretny warunek, taki jak pojawienie się elementu na stronie lub pliku w folderze.

Warto ograniczać testowanie interfejsu użytkowania

W automatycznych testach aplikacji webowej najwięcej czasu zajmuje ładowanie się strony w przeglądarce. Zastosowanie technologii headless (testy przeglądarkowe bez otwartego prawdziwego okna przeglądarki) lub zejście na poziom serwisów aplikacji i całkowite zaniechanie testów UI rozwiąże problem wolniejszego działania, ale nie zawsze jest możliwe do zastosowania. Rozwiązania umożliwiające testy headless, takie jak PhantomJS, nie gwarantują takiego samego zachowania aplikacji, jak w poszczególnych przeglądarkach. Dzieje się tak, ponieważ używają one innego silnika do renderowania stron www (WebKit). Trzeba wziąć również pod uwagę, że testy nie wspierają niektórych technologii, takich jak na przykład flash.

Dobrym rozwiązaniem problemu jest więc zejście z testami na poziom API. Jeżeli aplikacja wystawia interfejs, którego użycie jest identyczne z interakcją przez interfejs użytkownika, możemy znacząco zmniejszyć ilość testów przeglądarkowych na rzecz testów API, badając logikę biznesową z pominięciem części aplikacji wyświetlanej w przeglądarce. Różnica w prędkości może być znacząca, jednak nie badamy szczegółowo poprawności działania interfejsu użytkownika, co w zależności od projektu, może być głównym celem testów regresji. W wypadku zaś testów, których zadaniem jest ocena poprawnego zachowania kontrolek (jak na przykład w przeglądarkach), jesteśmy skazani na testy aplikacji otwartej w pełnej przeglądarce.  

Należy zrezygnować z testowania długich ścieżek (e2e)

Testy automatyczne powinniśmy wykorzystywać do testowania pojedynczych funkcjonalności. Dzięki temu dostaniemy szybką odpowiedź na pytanie, które z funkcjonalności w aplikacji działają poprawnie, a które nie. Automatyzowanie długich ścieżek, przechodzących przez wiele części aplikacji, sprawia, że test trwa długo, a same testy stają się mniej stabilne, ponieważ jest więcej miejsc, w których coś może pójść nie tak. Ponadto, w wypadku niepowodzenia w którejś części testu, kolejne kroki nie są w ogóle uruchamiane i nie mamy żadnej informacji o tym, czy części aplikacji działają poprawnie, czy nie. O ile nie uda nam się w satysfakcjonujący sposób rozwiązać tych problemów, sugeruję więc, by ścieżki „end 2 end” zostawić  testom manualnym albo w naszym zestawie testów automatycznych ograniczyć je do minimum.

Zrównoleglenie uruchamiania testów

Sposobem na pozornie niemożliwe do osiągnięcia rozciągnięcie czasu, jaki mamy na uruchomienie testów, jest uruchamianie ich równolegle. Prosty przykład pokazuje, jak duży zysk możemy mieć ze stosowania takiej strategii.

Jeżeli mamy 10 testów, z których każdy zajmuje średnio 3 minuty, puszczanie ich sekwencyjne potrwa 30 minut.

Uruchomienie równoległe – czas najdłuższego testu, czyli około 3 minuty.

Zrównoleglenie uruchamiania testów nie jest jednak proste i wymaga odpowiedniego zaprojektowania rozwiązania testów automatycznych, zarówno pod względem technicznym, jak i logicznym.  

Wymagania techniczne

Żeby można było bez problemów zrównoleglić testy automatyczne, technologia, w której je piszemy, musi obsługiwać wielowątkowość. Wbrew pozorom bowiem, nie wszystkie języki programowania, czy gotowe frameworki do automatyzacji, mają taką możliwość (robotframework, na przykład, potrzebuje zewnętrznych bibliotek). Zanim wybierzemy technologię, w której będziemy automatyzować, warto więc sprawdzić, czy nie napotkamy na problemy na tym polu, w chwili gdy  zabierzemy się za zrównoleglenie testów.

Drugie wymaganie to konieczność posiadania laboratorium z grupą maszyn, na których będą uruchamiane testy. Z mojego doświadczenia wynika, że na jednym komputerze lub wirtualnej maszynie można bezpiecznie uruchomić 5-10 przeglądarek z testami. Liczba ta jednak waha się w zależności od pamięci, jaka potrzebna jest na uruchomienie samej aplikacji w przeglądarce. Może się okazać, że mając kilkadziesiąt testów będziemy potrzebowali nie jednego, a kilku komputerów lub wirtualnych maszyn. Alternatywie możemy również skorzystać z usług udostępniających maszyny wirtualne z dowolną przeglądarką na żądanie. Przykładem może tu być SauceLabs, Gridlastic, czy Browserstack – na rynku jest bardzo dużo dostawców oferujących  podobne rozwiązania.  

Przykład:

Sposób rozwiązania powyższych problemów przedstawię na przykładzie testów automatycznych, które zaprogramowałem dla webowej aplikacji firm transportowych i spedycyjnych fireTMS.com. Testy automatyczne zostały napisane w Javie z wykorzystaniem JUnit, Maven, WebDriver i SauceLabs/Gridlastic.

Wielowątkowość:

Sama Java wspiera wielowątkowość, a dzięki zastosowaniu mavena i pluginu surefire jesteśmy w stanie z jednego miejsca w kompleksowy sposób sterować zrównolegleniem testów. Konfiguracja wygląda w następujący sposób:

Poszczególne parametry mają następujące funkcje:

useUnlimitedThreads – pozwala na określenie, czy chcemy użyć maksymalnej liczby wątków,

forkCount – określa, ile klas testowych ma się uruchomić jednocześnie,

threadCount – określa, ile metod testowych w ramach klasy ma się uruchamiać jednocześnie,

parallel – strategia zrównoleglenia (inne dostępne to m.in. classes, both, suites),

Surefire plugin ma jeszcze wiele ustawień, które pozwalają zarządzać sposobem równoległego uruchamiania testów. Należą do nich między innymi tak zaawansowane opcje, jak uzależnienie ilości wątków od ilości rdzeni procesora, na którym będę uruchamiane testy. Pełna dokumentacja znajduje się tutaj:

 

Laboratorium:

Do uruchamiania testów korzystałem z usługi Sauce Labs zamiennie z GridLastic. Poniższy schemat prezentuje zasadę działania tego typu rozwiązania:

Polecenia wysyłanie ze skryptu automatycznego są wysyłane na hub Selenium Grida na serwerze dostawcy usługi. Następnie w zależności od konfiguracji testu, powoływane są wirtualne maszyny z wymaganymi przeglądarkami i do nich przekazywane są konkretne instrukcje naszych testów. W przeglądarkach, na danym środowisku, uruchamiana jest nasza aplikacja webowa i w niej wykonywane są testy. Jeżeli jest taka konieczność, komunikację pomiędzy wirtualnymi maszynami a aplikacją można zabezpieczyć tunelem VPN.

Powyższa metoda daje ogromne możliwości zrównoleglenia testu.  Minusem jest jednak koszt, który w zależności od planu i ilości dostępnych wirtualnych maszyn, może wynieść od 20 do kilkuset dolarów miesięcznie.  

Niezależność testów

Kolejną ważną kwestią jest konieczność takiego zaprojektowanie testów, aby każdy z nich mógł być uruchomiony samodzielnie.

W każdej firmie, w której do tej pory pracowałem, na pewnym etapie pracy pojawiał się pomysł, że skoro mamy już test automatyczny do tworzenia obiektu w aplikacji, to może warto wykorzystać go także w teście edycji danych obiektu i teście kasowania danych. W rzeczywistości jednak takie rozwiązanie nie jest dobre – oprócz tego, że całkowicie uniemożliwia  równoległe uruchamianie testów, powoduje również zaciemnienie wyników testów. Zobaczmy dlaczego. Przykładowa sekwencja będzie wyglądała tak, jak poniżej:

TC1 – tworzenie użytkownika

TC2 – edycja danych użytkownika z TC1

TC3 – kasowanie użytkownika z TC1

W wypadku próby równoległego uruchomienia TC2 i TC3 będą miały wynik negatywny (nie znajdą użytkownika), ponieważ użytkownik, na którym będą chciały działać, zostanie utworzony dopiero po sukcesie TC1.

Co gorsza, nawet gdy trzy testy zostaną uruchomione sekwencyjnie, w wypadku błędu TC1 kolejne testy również skończą się niepowodzeniem lub w ogóle się nie uruchomią. W konsekwencji będziemy mieli jeden zgłoszony błąd i dwa testy o niewiadomym wyniku.

Gdy zapewnimy niezależność testów, nasze trzy testy będą wyglądały następująco:

TC1 – tworzenie użytkownika,

TC2 – tworzenie użytkownika (przez API), edycja danych użytkownika,

TC3 – tworzenie (przez API), kasowanie użytkownika.

TC2 i TC3 same dla siebie przygotowują dane testowe. Dzięki temu możemy wszystkie te testy uruchomić równolegle, oszczędzając mnóstwo czasu. Ponadto, jeżeli użytkownicy dla TC2 i TC3 zostaną utworzeni z pominięciem interfejsu użytkownika (np. za pomocą API), mamy gwarancję, że w przypadku błędu w TC1, pozostałe testy zostaną wykonane i zwrócą informację o działaniu lub niedziałaniu funkcjonalności edycji danych i kasowania użytkownika.

Przykład:

Tworzenie danych dla każdego testu zapewniamy za pomocą JUnitowej adnotacji @Before, w której wywołujemy po kolei API tworzące niezbędne dane:

  • tworzenie nowego tenanta poprzez API,
  • tworzenie oddziału tenanta,
  • tworzenie użytkownika dla danego tenanta,
  • tworzenie pozostałych, niezbędnych ustawień tenanta, dla wszystkich testów.

Jeżeli test potrzebuje dodatkowych danych, tworzymy je już w samej metodzie testowej.

 

Rezultat

Dzięki wprowadzeniu w życie opisanych dobrych praktyk oraz zrównolegleniu wykonania testów, jesteśmy w stanie w ciągu 30-40 minut przeprowadzić cały zestaw 240 testów, które w sekwencji trwałyby 12 godzin.

Łatwość utrzymania i dodawania nowych testów

Odpowiednie zastosowanie wzorca Page Object Pattern daje nam możliwość szybkiego utrzymania testów oraz łatwego dopisywania kolejnych. Nie do przecenienia jest również możliwość szybkiego wprowadzania do projektu  kolejnych inżynierów.

Są jednak elementy, których stosowanie wraz z Page Object Pattern decyduje o tym, czy nasz framework okaże się wystarczająco dobry w obliczu większych wyzwań, którymi dla testów UI są na przykład kompleksowe zmiany layoutu aplikacji lub problemy ze niesławnym Stale Element Reference Exception.

Opakowanie metod WebDrivera

Interakcja naszych testów z przeglądarką powinna być opakowana naszymi własnymi metodami. Dzięki temu akcje typu click(), getText(), getAttribute(), sendText() itp. będziemy mogli  rozszerzyć o oczekiwanie na odpowiedni stan elementu lub obsługę animacji ładowania strony aplikacji oraz pasek postępu. Odpowiednie zastosowanie dynamicznego oczekiwania na elementy rozwiąże nam również problem Stale Element Reference Exception.

Przykład:

Na początku wywołujemy metodę waitForLoader, która oczekuje na zniknięcie paska postępu ładowania aplikacji. Następnie wywołujemy waitForElementClickable, która czeka aż element stanie się możliwy do kliknięcia. W kolejnej linii tworzymy obiekt wait dynamicznego oczekiwania klasy WebDriverWait i ustawiamy timeout na 20 sekund. Kolejne linie to ustawienie kolejnej próby wykonania akcji na 200 milisekund oraz zapewnienie, że w wypadku wystąpienia wyjątków typu InvalidElementStateException i StaleElementReferenceException będą one ignorowane i nastąpi ponowna próba wykonania akcji z sekcji @Override aż do osiągnięcia sukcesu lub wyczerpania czasu timeouta.

Drugi przykład odnosi się do wrappera waitForElementClickable, który oprócz ocenienia, czy obiekt jest klikalny za pomocą warunku z klasy ExpectedConditions, w adnotacji @Override sprawdza również atrybuty elementów charakterystyczne dla testowanej aplikacji. Kiedy sam korzystałem z tej metody, pomimo spełnienia warunku ExpectedConditions.elementToBeClickable, niektóre kontrolki były ustawione jako nieaktywne w niewidoczny dla klasy ExpectedConditions. Dopiero wprowadzenie dodatkowych warunków całkowicie rozwiązało problem zbyt wczesnego klikania w przycisk przez WebDrivera.

Wspólne lokatory

Poniższa sekcja odnosi się jedynie do przypadków, w których z powodu ograniczeń aplikacji programiści nie są w stanie umieścić identyfikatorów węzłów html w dowolnym miejscu.

Page Object Pattern zakłada trzymanie lokatorów do kontrolek na stronie w jednym miejscu. Warto jednak pójść o krok dalej. Jako że wszystkie aplikacje webowe są tworzone za pomocą frameworków, bardzo prawdopodobne, że nasze lokatory będą mieć części wspólne. W takim wypadku należy te części wspólne przenieść do innego miejsca, np. do abstrakcyjnej klasy strony, aby można było nimi łatwiej zarządzać. W naszej aplikacji wszystkie tabelki są osadzone w następujący sposób:

Jak widzimy, pomiędzy węzłem z unikalnym id a węzłem tabeli mamy ścieżkę xpath – “/div[2]/div[1]/table/tbody”. Ścieżkę tę możemy przechować w zmiennej w abstrakcyjnej klasie strony.

Następnie możemy wykorzystać ją na wszystkich stronach przy tworzeniu lokatorów:

lub

Dzięki takiemu rozwiązaniu zmiana we frameworku lub layoucie strony pozwala na szybką korektę ścieżek pomiędzy węzłem z identyfikatorem a węzłem tabelki.

Abstrakcyjne metody

Jeżeli to możliwe, w testach należy wydzielać metody, które mogą być zastosowane do standardowych obiektów w aplikacji, niezależnie od tego, na jakiej stronie się znajdują. Dzięki temu dodanie kolejnej strony do naszego projektu w POP będzie krótsze, a kodujący ją inżynier nie będzie musiał rozwiązywać już rozwiązanych problemów.

Przykład:

Oto przykład metody z abstrakcyjnej klasy strony. Przyjmuje ona lokator do tabel, wartość, którą chcemy sprawdzić, oraz numer wiersza i kolumny, pod którą chcemy ją znaleźć.

Rezultat

Na podstawie własnego doświadczenia mogę potwierdzić, że stosując wrappery, wspólne lokatory i abstrakcyjne metody jesteśmy w stanie dopasować ponad 200 testów automatycznych po całkowitej zmianie layoutu w trzy dni.

Stabilność i elastyczność:

Pod pojęciem „stabilne testy” rozumiem takie, które nie kończą się niepowodzeniem z niewiadomego powodu. Mianem elastyczności określam zaś możliwość uruchamiania testów na dowolnym środowisku, z dowolnymi ustawieniami, bez konieczności zmiany samego kodu testów, a także możliwość szybkiego rozszerzania testów o nowe moduły.

Stabilność

Aby zapewnić stabilność, zarówno w przypadku opensource, jak i rozwiązania komercyjnego, należy korzystać ze sprawdzonego narzędzia z dobrym dostępem do wsparcia. Jeżeli bowiem natrafimy na problem niestabilności testu, którego sami nie będziemy w stanie rozwiązać, możemy znaleźć się w sytuacji, z której trudno będzie nam wybrnąć. W tym kontekście istotne znaczenie ma popularność technologii, której używamy. Im więcej osób używa danej metody, tym większa szansa, że rozwiązanie naszego problemu znajdziemy na stronach typu Stack overflow. W ten sposób ograniczamy ryzyko wystąpienia nieprzewidzianego zachowania testów.

Drugi istotny dla stabilności czynnik to wspomniane wcześniej użycie wrapperów i centralne zarządzanie interakcją z przeglądarką. Korzystanie z tych rozwiązań pozwoli nam całkowicie usunąć takie, wynikające z charakterystyki WebDrivera, problemy ze stabilnością testów, jak brak wsparcia dla technologii asynchronicznych (np. AJAX) oraz pozbyć się kłopotów związanych z oczekiwaniem na elementy w odpowiednim stanie.

Elastyczność

Java i maven pozwalają spełnić kryterium elastyczności dzięki opcji parametryzacji oraz możliwości rozszerzania testów o nowe moduły.

Parametryzacja

Dzięki odpowiedniemu skonstruowaniu abstrakcyjnej klasy testu oraz pliku POM.xml jesteśmy w stanie sterować parametrami wejściowymi do naszych testów w miejscu, w którym testy mają być uruchomione, oraz określać, na jakim środowisku i w ilu wątkach testy zostaną uruchomione równocześnie.

Przykład:

Zastosowanie mavena do uruchamiania testów pozwala na przesłanie do testów grupy parametrów:

Wykorzystanie zmiennych w abstrakcyjnej klasie testu:

Obiekt WebDrivera jest powoływany z wyborem serwera, na który mają być wysyłane jego żądania. Serwer jest określany zgodnie z parametrem przesłanym przez mavena:

Metoda getInitialUrl pobiera URL aplikacji z parametru przesłanego przez mavena. Metoda ta jest później używana jako argument metody logowania w aplikacji:

Przekazanie zmiennych do POM.xml realizowana jest w następujący sposób:

Dzięki parametryzacji głównej klasy testu i pliku POM.xml jesteśmy w stanie zmieniać wszystkie istotne parametry środowiskowe testu bez ingerowania w kod, co znacznie przyspiesza i ułatwia przełączanie się pomiędzy środowiskami testowymi.

Możliwość rozszerzania testów o nowe moduły

Wyobraźmy sobie, że nasza aplikacja uzyskuje moduł, który pozwala na wysyłanie e-maili lub drukowanie dokumentów pdf. Żeby zapewnić automatyczne testowanie tych obszarów, musimy mieć możliwość łatwego importowania nowych bibliotek do naszego projektu testów automatycznych.  Niektóre frameworki opensource mają ograniczoną liczbę tworzonych przez społeczność bibliotek z ograniczonym wsparciem i powolnymi reakcjami na zmiany. Rozwiązania komercyjne również nie wspierają wszystkich technologii. Często chęć użycia dodatkowych modułów wiąże się z koniecznością zakupu dodatkowej licencji. Wybierając rozwiązania dla naszych testów automatycznych, warto wziąć te kwestie pod uwagę.

Przykład:

W wypadku użycia Javy działanie sprowadza się do znalezienia odpowiedniej biblioteki dla naszego celu (zazwyczaj jest ich kilka) i wypadku, gdy jest ona w centralnym repozytorium mavena, oraz dodania wpisu do POM.xml, np.:

 

Po przebudowaniu projektu mamy dostęp do wszystkich klas biblioteki.

 

Podsumowanie

Szybkość, zaufanie do wyników i możliwość łatwego rozszerzania to cechy, które powinny mieć testy automatyczne, aby być przydatnymi w Continuous Integration. Praca na kilku wersjach aplikacji dziennie powoduje, że testy automatyczne niespełniające tych parametrów nie będą w stanie szybko podać informacji o obecności błędów regresji w aplikacji.  

Komentarze

Napisz do nas

Przeglądając stronę wyrażasz zgodę na przetwarzanie swoich danych osobowych pozostawianych w czasie korzystania przez Ciebie z Serwisu oraz innych parametrów zapisywanych w plikach cookies przechowywanych na urządzeniu, z którego korzystasz w celach marketingowych, w tym na profilowanie i w celach analitycznych przez "IT KONTRAKT" spółka z ograniczoną odpowiedzialnością z siedzibą we Wrocławiu, ul. Gwiaździsta 66, 53-413 Wrocław i Zaufanych Partnerów IT KONTRAKT Sp. z o.o.