Framework testów w Selenium z paralelizacją, obsługą wielu przeglądarek i internacjonalizacją – część 2

Aktualności | Artykuły | Blog | QualityMinds | Testing

W poprzednim artykule pokazaliśmy Wam pierwsze etapy pisania frameworku z niezależnymi testami: konfigurację zależności i tworzenie podstawowych klas. Następny etap pracy to logowanie:

Krok 3: logowanie

Jeśli wszystko poszło zgodnie z planem, powinniśmy mieć uruchomione testy, które generują dużo niepotrzebnego hałasu w konsoli. Dodatkowo, logi nie są zapisywane, co utrudnia proces debugowania.

Aby rozwiązać ten problem, możemy dodać konfigurację logowania i zapisywanie logów do pliku. Użyjemy w tym celu Logback. Należy dodać odpowiednie pliki konfiguracyjne dla Logback (np. logback-test.xml), gdzie zdefiniujemy reguły dotyczące poziomów logowania, docelowych plików logów, itp.

Poniżej znajduje się przykładowa konfiguracja Logbacka, do umieszczenia w resources



To jak konfigurować Logback, to temat na oddzielny artykuł. Powyższą konfigurację skopiujmy do resources, a poniższy plik do package utils.

Krok 4: parametryzacja

Teraz czas na zmodyfikowanie pliku BaseTest, tak by przyjmował parametry z różnych źródeł. Napiszemy kod, który pozwala czytać parametry z pliku XML, jednak można je nadpisać podając JVM Options, a najwyższy priorytet przyjmują parametry wprowadzane z CLI

Zatem <parameter name=browser value=chrome> ma być nadpisywany przez polecenie z CLI – Dbrowser=EDGE.

Zmodyfikujmy więc klasę BaseTest, tak by obsługiwała powyższe założenia: W 1 kolejności dodajemy 2 pola BrowserType DEAFULT_BROWSER = BrowserType.CHROME i boolean DEFAULT_HEADLESS = TRUE.

Następnie modyfikujemy metodę before, tak by w przypadku podania zbyt małej bądź zbyt dużej liczby argumentów, wybrała wynikający z precedencji, bądź domyślny.

Najpierw potrzebna będzie nam metoda boolean isNullOrBlank(String), która sprawdza, czy argument nie jest null bądź pusty.

W 32 linijce mamy funkcje T resolve(String String T), która odpowiada za precedencje parametrów.

Jak widać ciało metody before zostało zmodyfikowane tak by zwracać właściwy driver na podstawie wyżej wymienionych zmiennych, które definiują instancje Drivera.

Krok 5: Page Object Model

Przestrzegamy branżowych zasad i oczywiście tworzymy framework korzystając z page object model. Po wejściu na stronę qualityminds.com widzimy, że składa się ona z podstron i wspólnego górnego paska nawigacyjnego (TopBar). Podczas naszych testów będziemy musieli otworzyć stronę główną (index) oraz podstronę automatyzacji. Więc będziemy mieli do wykonania 3 PO – HomePagePO, AutoTestPO, TopBarPO – ten ostatni umieścimy jako pole (field) w poprzednich Page Objectach, korzystając z kompozycji.

Dodatkowo, każdy Page Object (ale nie TopBarPO) zawiera metody load() i waitForPageToLoad(), takie, że load() wywołuje waitForPageToLoad(). W przypadku internacjonalizacji, metody te muszą przyjmować I18n jako parametr, by wiedzieć z jakim językiem porównywać widoczne treści. Te dwa interfejsy zawierają te metody, a interfejs Loadable świetnie pasuje do stron, które wyglądają tak samo w każdym języku lub w przypadku, gdy elementy lokalizacji nie są istotne z punktu widzenia automatycznego testu. Umieśćmy je w pakiecie pages.

Designu samej klasy BasePage nie będę tłumaczył szczegółowo, kod dostępny jest poniżej

Krok 6: lokalizacja

Zanim rozszerzymy klasę BasePage na konkretne podklasy, potrzebujemy narzędzia, które umożliwi nam porównywanie znalezionego tekstu na stronie z oczekiwanym. Możemy oczywiście zdefiniować 18 prywatnych statycznych zmiennych typu String, ale w rzeczywistych frameworkach wykonujemy dużo więcej asercji, a każda zmiana tekstu na stronie wymagałaby modyfikacji kodu. Dlatego znacznie łatwiej i bezpieczniej jest przechowywać kod w zewnętrznym pliku, na przykład w formacie JSON, i odczytywać stamtąd oczekiwane wartości tekstowe.

Określmy, jakie stringi będą nam potrzebne do asercji i zapiszmy je w pliku json: Teraz napiszemy kod, który pomoże nam czytać z pliku json, potrzebne będą do tego 2 klasy – StringLoader – będzie to singleton ładujący plik json do pamięci oraz Parser – klasa, która plik json zamieni na obiekt JsonNode.

Zacznijmy od klasy Parser, umieśćmy ją w package utils razem z ThreadIdConverter, będzie to prosta implementacja ObjectMapper z Jacksona. Zawiera ona tylko jedną metodę publiczną, która ma zamienić plik w JsonNode. W argumencie przyjmuje jego lokalizacje w classpath.

Teraz napiszmy klasę StringLoader również w utils. Klasa jest lazy loaded singletonem, korzystającym z Initialization-on-demand holder idiom.

Konstruktor singletona ładuje plik strings.json do pamięci, a metoda JsonNode strings(I18n) pozwala na otrzymanie obiektu z pliku json zawierającego dane dla konkretnej internacjonalizacji.

Takie rozwiązanie działa, jest jednak bardzo niewygodne w użyciu, za każdym razem, gdy chcielibyśmy np. przywołać konkretny string musielibyśmy pisać:

StringLoader.getInstance().strings(i18n).get(key).asText();

Wobec tego zróbmy interfejs z jedną metodą default, zawierającą właśnie powyższy sparametryzowany kod. Każda klasa, która zaimplementuje ten interfejs nie będzie miała problemów z wyciąganiem stringów z json.

A oto implementacja Interfejsu Localizable (zapiszmy go w package pages): Zauważmy, że zdefiniowałem już 6 kluczy stringów jako stałe, więc używanie ich nie powinno być problemem. Ten interfejs będzie zaimplementowany przez każdy PO, w którym są elementy, których tekst należy sprawdzić asercją, czyli HomePagePO i AutoTestPO, które zaimplementujemy w następnym korku:

Krok 7: konfiguracja PO

Teraz rozszerzymy klasę BasePage na poszczególne Page Objecty. Ten krok nie zawiera nic skomplikowanego, dlatego też ograniczę się do krótkiego opisu i podania gotowego kodu. Tworząc metody w Page Objectach polecam korzystać z tzw. Fluent API, czyli kazać metodom zwracać this, bądź, gdy metoda powoduje zmianę strony (np. kliknięcie w menu), nową instancję tego Page Objectu, który reprezentuje abstrakcję strony, na którą przechodzimy.

  1. Zanim jednak ruszymy jakiekolwiek PO, szybko wróćmy do dawno stworzonego enuma, I18n. Dodajmy stałe abbr, url i href, co ułatwi pisanie metod, które przyjmują I18n jako parametr.

  2. HomePagePO – implementuje Loadable, LocaleLoadable oraz Localizable: Metody load() i waitForPageToLoad() (wywoływana przez load()) są przydatne do otwierania strony bez określania języka – w takim przypadku strona jest ładowana domyślnie. Mamy również te same dwie metody z parametrem typu I18n, które używamy, gdy zmieniamy język, ponieważ wtedy ładowany jest inny adres, a metoda waitForPageToLoad(I18n) sprawdza, czy strona została poprawnie załadowana w różnych językach, zależnie od parametru I18n.

    Łatwo zauważyć, że metoda waitForPageToLoad() korzysta z metody default z interfejsu Localizable, aby porównywać tekst ze strony z oczekiwanym tekstem z pliku json.

    Dodatkowo jako pole klasy, mamy instancję TopBarPO, która umożliwia dostęp do górnego paska w dowolnym momencie korzystania z HomePagePO, co wykorzystamy w testach.

  3. AutoTestPO – implementuje LocaleLoadable oraz Localizable: Podobnie jak w HomePagePO, komponentem klasy jest również TopBarPO.

  4. TopBarPO – jedyny PO, który nie jest abstrakcją nad danym widokiem strony, a komponentem widocznym z każdego widoku, dlatego też jest wkomponowany w poprzednie PO.

Krok 8: testowanie

Mamy już wszystkie metody niezbędne do nawigowania po stronie, jedyne co nam pozostało, to wywołać je i mamy gotowy test automatyczny. W package test zróbmy klasę LocalisationTest z metodą void verifyTestAutomationPageTranslation(I18n), ponieważ taki sam test musi być wykonany trzykrotnie dla każdego z języków. W tym celu skorzystamy z DataProvidera, któremu każmy zwrócić I18n[] czyli I18n.values(). Ustawmy adnotacje @DataProvider(parallel = true), dzięki czemu, póki starczy wątków, metody mogą być uruchamiane równolegle.

Sam test jest bardzo prosty, dzięki fluent API raz wywołujemy new HomePagePO(driver()), a potem już do końca testu chainujemy metody w tej kolejności (poniżej opis krok po kroku). Dzięki użyciu DataProvidera, wykonają się one 3 razy, za każdym razem z innym parametrem i18n.

  1. Załaduj stronę główną
  2. Zamknij popup z Cookies.
  3. Zwróć instancje TopBarPO
  4. Sprawdź, że obecny język to angielski
  5. Najedź myszą na menu języka
  6. Zmień język na i18n (ta metoda nie robi nic, jeśli i18n == I18n.ENGLISH).
  7. Sprawdź czy obecna ikonka języka odpowiada i18n
  8. Najedź myszą na menu Portfolio
  9. Kliknij na podmenu automation tests
  10. Poczekaj, aż strona się załaduje w języku i18n
  11. Sprawdź przycisk contact us dla języka i18n

I tyle.

Zmodyfikujmy jeszcze plik testng.xml I uruchamiamy testy – wszystko powinno działać.

Jak zwykle zachęcam do pobawienia się plikiem konfiguracyjnym, dopisania własnych testów (które chociażby tylko miały pisać wiadomość na konsoli) i posprawdzania różnych wariantów paralelizacji.

Czytelniczka lub czytelnik na pewno zauważą, że głębokość, na której chcemy dokonywać paralelizacji, wymaga od nas coraz większej dyscypliny w budowaniu frameworku. Uruchamianie testów na poziomie poszczególnych metod wymaga największej dyscypliny w pisaniu kodu.

Na stronie repozytorium, na gałęzi main jest jeszcze dostępny workflow dla GitHuba oraz plik bat uruchamiający skrypt PowerShell, który natychmiast wyłącza wszystkie sesje Chrome, Edge, Firefoxa i ich driverów – przydatne, gdy podczas debugowania zostaje nam mnóstwo niedomkniętych okienek.

0 komentarzy

Napisane przez

Rafał Michalski