Benjamin Franklin kiedyś powiedział, że „czas to pieniądz”. Od tego czasu minęło już ponad 270 lat i zdaje się, że z każdym dniem powiedzenie to staje się coraz bardziej aktualne. Oszczędzając czas, oszczędzamy również pieniądze, zarówno własne, jak i firmy, dla której pracujemy. W tym artykule przedstawiam 3 sposoby jak możesz przyspieszyć testy automatyczne w swoim projekcie.
W procesie produkcji oprogramowania jest bardzo wiele miejsc, których optymalizacja przynosi bardzo wymierne korzyści czasowe, a co za tym idzie, również finansowe. W tym artykule skupimy się na optymalizacji kodu testów automatycznych, dzięki której zyskamy nie tylko cenne godziny poprzez skrócenie czasu wykonywania, ale również większą stabilność testów. Nie będziemy poruszać kwestii równoległego uruchamiania czy korzystania z trybu „headless” przeglądarek, co również powinno wpłynąć pozytywnie na czas egzekucji.
Słowem wstępu zaznaczę jeszcze, że technologia nie będzie miała tu żadnego znaczenia. Bardziej chodzi o wyjaśnienie implementacji poszczególnych kroków testów. Będziemy automatyzować testy UI używając języka Java. Link do repozytorium, z którego będę podawał przykłady – https://github.com/maredenek/test-optimization – polecam mieć otwarty projekt podczas czytania. Czasy jakie podaje to średnia z trzech kolejnych uruchomień testów.
Za przykład weźmy 2 proste scenariusze dla sklepu http://automationpractice.com/:
1. Funkcja wylogowania użytkownika
2. Funkcjonalność usuwania adresu z konta
W podejściu BDD wyglądałyby następująco:
1. Scenariusz: Zalogowany użytkownik powinien być w stanie wylogować się z konta
- Given: Użytkownik jest zalogowany
- When: Użytkownik kliknie przycisk ‘Sign out’ na górnej belce
- Then: Użytkownik został poprawnie wylogowany z konta
2. Scenariusz: Zalogowany użytkownik powinien mieć możliwość usunięcia adresu przypisanego do konta
- Given: Użytkownik ma zapisany przynajmniej jeden adres
- When: Użytkownik kliknie przycisk ‘Delete’ na kafelku z adresem oraz potwierdzi chęć usunięcia
- Then: Adres został poprawnie usunięty
Niby bardzo proste założenia i krótkie testy, ale jak to zazwyczaj bywa, diabeł tkwi w szczegółach! Dlatego proszę żebyś Ty, Czytelniku, zatrzymał się chwilę w tym miejscu i zastanowił się jak Ty byś zaimplementował powyższe testy… Tymczasem my lecimy dalej!
1. Po pierwsze primo: czytanie danych testowych!
Doskonale zdaję sobie sprawę, że istnieje wiele sposobów przechowywania danych wykorzystywanych w testach, jednak jest jeden wspólny mianownik – trzeba je w jakiś sposób pozyskać! W przypadku przechowywania pliku, należy ten plik otworzyć, przeczytać dane a następnie plik zamknąć. W przypadku bazy danych należy wykonać zapytanie, co również krótką chwilę zajmuje. Jakiegokolwiek podejścia nie stosujecie, pamiętajcie o jednym:
CZYTAJCIE DANE TESTOWE TYLKO RAZ PRZED WSZYSTKIMI TESTAMI
O ile oczywiście nie istnieje żaden uzasadniony powód, dla którego musimy po raz kolejny dane wyciągnąć z naszego źródła, bo np. zostały w trakcie testów zmienione. W klasie TestData metody getUsersData() oraz getEnvironmentData() zwracają dane, które są zaczytywane z plików znajdujących się w katalogu resource. Wielokrotnego otwierania pliku możemy uniknąć np. poprzez zastosowane lazy loadingu.
private final Lazy<List<User>> usersData = Lazy.of(() -> getFileDataAsListOf(User.class, "usersData.json"));
Zysk czasowy jest raczej niewielki, jednak w przypadku uruchamiania testów równolegle, może wystąpić zakleszczenie, tj. jeden wątek otworzył plik i przed jego zamknięciem inny wątek próbuje otworzyć ten sam plik.
2. Po drugie primo: korzystanie z szybszych interfejsów!
Mam tu na myśli API oraz wykonywanie poleceń bezpośrednio na bazie danych. Jednak czasami z poziomu maszyny, na której uruchamiane są testy, nie mamy dostępu do bazy + wymagana jest znajomość składni SQL/NoSQL, skupimy się na żądaniach do API. Jak można zauważyć, żaden z powyższych scenariuszy nie ma na celu np. sprawdzania formularza logowania. Nie róbmy tego zatem. Skupmy się na tym co jest faktycznie przedmiotem testów, tj. odpowiednio akcja wylogowania oraz akcja usuwania adresu. Możliwe jak największą pozostałą część starajmy się zawsze wykonać używając szybszego interfejsu. Zyskamy wówczas również na stabilności testów, ponieważ uniezależniamy się od kolejnych warstw aplikacji (wykonując polecenia na bazie pomijamy front-end i back-end, strzelając do API pomijamy front-end). Załóżmy, że z jakiegoś powodu jedynie na „froncie” nie działa formularz logowania. Używając API zalogujemy użytkownika i z powodzeniem przeprowadzimy testy, które wymagają sesji użytkownika. Logując formularzem testy się posypią na samym początku nie spełniając swojej roli.
- Logowanie użytkownika
Przesiadając się z logowania po UI (LoginForm::fillAndSendLoginForm()) na logowanie po API (PostAuthenticateApiRequest::sendRequest()) zyskałem łącznie ok. 10 sekund. Jednak tu również należy mieć na uwadze, że nie ma sensu logować użytkownika przed każdym testem. Można to zrobić tylko jeden raz, a następnie wykorzystywać tę samą sesję. Tu również możliwości cache’owania jest wiele. Ja używam lazy loadingu. Spójrz proszę na pole sessionCookie klasy User, a dokładnie na wrapper Lazy. Tu odsyłam do dokumentacji biblioteki Vavr.io. Pole to inicjalizowane jest w klasie TestData.
- Dodawanie adresu do usunięcia
Tu podobnie jak w przypadku logowania, formularz dodawania adresu nie jest przedmiotem testu, w związku z czym śmiało możemy wykonać tę akcję poprzez API.
(AddAddressForm::fillAndSendFormUsing()) na API (PostAddAddressApiRequest::sendRequest()) zyskałem ok. 5 sekund.
3. Po trzecie primo (Ultimo!): otwieranie przeglądarki!
Bardzo często spotykam się z implementacjami, gdzie przed każdym testem przeglądarka jest otwierana, następnie zamykana zaraz jego wykonaniu. Użytkownicy końcowi nie zamykają okna po każdej akcji żeby zaraz otworzyć przeglądarkę ponownie, więc nie róbmy tego w testach. Tu zmiana jest bardzo prosta, jednak jej wygląd zależy od frameworka do zarządzania testami. Ogólnie rzecz ujmując, należy przenieść otwieranie przeglądarki z @BeforeEach do @BeforeAll. Zamykanie natomiast z @AfterEach do @AfterAll. Jest to chyba najprostsza z omawianych optymalizacji, jednak daje największy zysk. W przypadku omawianych testów ok. 10 sekund, jednak z doświadczenia wiem, że w realnym świecie jest to ładnych kilkanaście do kilkudziesięciu procent czasu trwania testów.
Podsumowując – po zastosowaniu wszystkich powyższych zasad, czas wykonywania przykładowych testów spadł z ~50s do ~30s (oszczędność ok. 40%).
Mam szczerą nadzieję, że powyższe wskazówki okażą się Wam pomocne podczas automatyzacji testów. Implementacji metod z premedytacją tu nie wklejałem, żeby nie wydłużać artykułu oraz żeby zachęcić do zajrzenia do repozytorium. Myślę, że spora część z Was może znaleźć tam inspiracje i rozwiązania problemów jakie napotkali, a które nie zostały omówione w tym tekście.
O autorze:
Mariusz Czabaj – QA Automation Engineer związany z obszarem testów od ponad 9 lat. W Softie prowadzi kurs Tester Automatyzujący.