Duplikaty i zmiany
Wyobraźmy sobie, że otrzymujemy zadanie przetestowania głupkowatej aplikacji, której mechanikę prezentuje poniższy diagram:
Użytkownik po zalogowaniu się może wybrać jedną (lub wiele) opcji, dodać (lub nie) komentarz oraz potwierdzić działanie. W rezultacie trafia na widok prezentujący informację o sukcesie lub porażce.
Ten głupkowaty, ale prosty, mechanizm może wygenerować wiele przypadków testowych (wystarczy się zastanowić, ile może być kombinacji opcji i komentarzy (oraz ile różnych komentarzy można wpisać).
Wyobraźmy sobie dalej, że piszemy skrypty testowe tak jak zaprezentowałem w poprzednim wpisie, czyli w każdym teście piszemy „find_element(locator).click()”, „find_element(locator).send_keys(text)”, itd.. Łatwo można zauważyć, że prawie każdy test będzie zawierał duplikaty z wypełniania formularza logowania czy, zaznaczania opcji. W efekcie mamy kilkanaście skryptów testowych, których pojawiają się te same fragmenty kodu.
Ostatnie wyobrażenie – developerzy zmieniają input odpowiedzialny za wprowadzenie loginu. Oczywiście istnieje szansa, że nasz lokator jest pancerny i nic mu się nie stanie – dalej będzie wskazywał na odpowiedni element. Bardziej jednak prawdopodobnym jest, że przestanie działać (zwłaszcza, jeżeli zmianie uległo Id elementu lub Accesibility Label). Czeka nas zatem zmiana lokatorów w kilkunastu (kilkudziesięciu?) miejscach. Im bardziej newralgiczny punkt aplikacji – tym tych zmian może być więcej.
Wzorce
Nasze problemy nie są niczym szczególnym, natknęli się na nie pierwsi testerzy E2E, natykać się na nie będą kolejne pokolenia – nic dziwnego, że powstały zbiory dobrych praktyk, które pozwalają ich uniknąć. Wzorce projektowe porządkują każdy aspekt pisania oprogramowania (a testy automatyczne, czy to się podoba programistom czy nie – są oprogramowaniem), pozwalają na jego łatwiejsze zrozumienie, utrzymanie i rozwój. Jednym z najczęściej używanych i najszybciej wprowadzanych jest Page Object Pattern.
POP
O co chodzi?
Page Object Pattern, to wzorzec, którego geneza wywodzi się z testów w środowisku Web (stąd nazwa), zakłada on stworzenie osobnych klas dla każdej strony (lub znaczącego jej elementu – na przykład rozbudowanego formularza). Wprowadzenie tej dodatkowej warstwy abstrakcji pozwala na zminimalizowanie ilości duplikowanego kodu. W poszczególnych klasach definiujemy lokatory elementów właściwych dla danej strony oraz akcje na niej wykonywane:
Dobrą praktyką jest również wykorzystanie dziedziczenia, dzięki czemu nie trzeba duplikować fragmentów kodu dotyczącego interakcji z elementami występującymi na wielu podstronach (np. menu wyświetlanego na każdej podstronie). Osobiście polecam również stworzenie bazowej klasy, w której można zamieścić wszystkie podstawowe interakcje z elementami, rozmaitego rodzaju hacki i waity. Na przykład, jeżeli mamy do czynienia ze stroną, w której elementy ładują się asynchronicznie, w klasie bazowej możemy zdefiniować własną metodę odpowiadająca za klikanie elementu, obejmującą również czekanie na jego pojawienie się.
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BaseView: DEAFAULT_TIMEOUT= 5 def __init__(self, driver): self.driver = driver def click(self, locator): self.wait_for_element(locator).click() def wait_for_element(self, locator, timeout=default_timeout): element = WebDriverWait(self.driver, timeout=timeout).until(EC.presence_of_element_located(locator)) return element
Dzięki temu, w każdej klasie dziedziczącej po BaseView, możemy korzystać z metody click(), za każdym razem mając pewność, że będziemy czekać na wyświetlenie interesującego nas elementu.
Mimo swojej webowej genezy – Page Object Pattern, sprawdza się znakomicie również w przypadku aplikacji mobilnych. Zdefiniowanie klas odpowiadających konkretnym widokom aplikacji pozwala nam zaoszczędzić mnóstwo czasu związanego z tworzeniem nowych przypadków testowych i zapewnieniem działania już istniejących.
Na koniec kilka zasad, których wypada się trzymać:
- Nie należy umieszczać asercji w klasie strony/widoku – walidacja widoków powinna odbywać się w teście;
- Akcje wykonywane na stronie (zdefiniowane w metodach) najczęściej powinny zwracać inne obiekty (odpowiadające kolejnym widokom) np. metoda odpowiadająca za zalogowanie użytkownika powinna zwracać obiekt odpowiadający stronie startowej;
- Jedna klasa nie musi odzwierciedlać całej strony – może ograniczać się do jej znaczącego fragmentu. Dotyczy to zwłaszcza sytuacji, w której zamieszczenie wszystkich elementów i akcji na stronie w jednej metodzie sprawiłoby że rozrosłaby się ona do nieczytelnych rozmiarów.
Więcej o Page Object Pattern można przeczytać między innymi tu.