React Native i automatyzacja testów E2E
W artykule poruszę temat automatyzacji testów E2E aplikacji mobilnych, pisanych przy użyciu React Native. Rozwiązanie w nim zaprezentowane ma rację bytu w przypadku, kiedy aplikacja, którą mamy testować tworzona jest z myślą o iOS i Androidzie. W sytuacji, kiedy React Native jest wykorzystywane do stworzenia aplikacji na jedną platformę — lepiej pozostać przy rozwiązaniach jej dedykowanych.
Mateusz Bernat. QA Tech Lead w Acaisoft. Niedoszły polonista, od sześciu lat związany z testowaniem oprogramowania. Aktualnie Tech Lead zespołu QA w Acaisoft, gdzie zajmuje się, między innymi, tworzeniem frameworków do automatyzacji testów API oraz frontu aplikacji mobilnych i webowych.
W wolnym czasie biega (bo musi) i brzdąka na ukulele (bo lubi).
Rynek aplikacji mobilnych od lat rozwija się dynamicznie, osiągając w 2018 wartość ponad 65 mld $ (kwota ta dotyczy wyłącznie pobrań i płatności wewnątrz aplikacji!). Prawie drugie tyle wart jest rynek związany z ich wytwarzaniem. Pośród systemów operacyjnych na urządzenia mobilne, jedynymi liczącymi się graczami są obecnie Apple i Google, dzieląc go ~22% dla iOS i ~75% dla Androida. Ponadto środowisko Androida charakteryzuje się dużo większą fragmentacją (wynikającą ze znacznie niższego stopnia aktualizacji OS niż w przypadku iOS), stawiając przed zespołami QA dodatkowe wyzwania.
Te miliardy dolarów czekają do podziału na tych, którzy najszybciej dostarczą najlepszą jakościowo (i oczywiście interesującą) aplikację — stąd tak duże zapotrzebowania na rozwiązania, które pozwolą na przyspieszenie procesu wytwarzania oprogramowania. Jednym z nich jest, zaproponowany przez Facebooka, React Native, pozwalający na pisanie natywnych aplikacji na iOS i Androida przy użyciu Javascript. Dzięki niemu, programiści mogą, tworząc jeden kod, przygotować aplikację, która będzie (lub powinna) działać na obu platformach.
Wydawać by się mogło, że React Native to kolejna odsłona aplikacji hybrydowych — to wrażenie jest jednak pozorne, React Native nie wykorzystuje WebView i oferuje znacznie większe możliwości niż aplikacje hybrydowe.
Rys 1. Renderowanie natywnych widoków aplikacji React Native
Zatem, nasz wspomniany kontekst to: znaczące skrócenie etapu programowania w procesie wytwórczym, połączone z koniecznością przetestowania aplikacji na rozdrobnionym rynku mobilnym (nie zapominajmy przecież o mnogości urządzeń, nie tylko wersji systemów operacyjnych!).
Spis treści
Co z tym zrobisz, testerze?
Przyspieszenie programistycznego etapu wytwarzania aplikacji mobilnych nie idzie niestety w parze z przyśpieszeniem etapu testowania — wręcz przeciwnie: ten sam zespół programistów może dostarczać blisko dwa razy więcej funkcjonalności niż w przypadku rozwiązań natywnych. Rozwiązaniem problemu, które większości przychodzi do głowy jest magiczne słowo: automatyzacja!
I jest to rozwiązanie, które może przynieść wiele korzyści, pod warunkiem że jest wprowadzane z głową i na rozsądnym etapie pracy z projektem.
Appium
Planując automatyzację testów aplikacji mobilnych, prawdopodobnie, chcielibyśmy móc skorzystać z rozwiązania, które — podobnie jak w przypadku programistów — pozwalałoby nam na napisanie jednego kodu testów i uruchamianie ich na obu platformach bez (lub z minimalnymi) zmianami.
To było głównym kryterium, którym kierowaliśmy się przy planowaniu automatyzacji testów aplikacji tworzonych w Acaisoft, kolejnym bardzo ważnym czynnikiem była możliwość odpalania testów zarówno na symulatorach/emulatorach, jak i urządzeniach fizycznych oraz łatwa integracja z platformami udostępniającymi przeprowadzania testów w chmurze.
Działanie Appium można porównać do Selenium (na którym jest oparte). Klient — w jednym ze wspieranych języków — komunikuje się z serwerem, a ten z urządzeniem, na którym uruchamiane są testy:
Rys 2. Architektura Appium
Instalacja
Appium można zainstalować w dwóch wersjach: Serwer i Desktop, wersja Desktop, dodatkowo zawiera graficzny interface, pozwalający na inspekcję elementów aplikacji. Pakiety instalacyjne są dostępne na stronie projektu, możliwa jest również instalacja przez npm.
Testy
Testy E2E aplikacji mobilnych, tworzonych w oparciu o Appium przypominają testy tworzone w Selenium, ich implementację warto oprzeć o Page Object Pattern (czy w tym przypadku Screen Object Pattern), w oddzielnych klasach definiując ich lokatory i akcje. Jako przykład, możemy wyobrazić sobie aplikację czatów, która posiada tylko trzy widoki:
– listę czatów,
– okienko czatów,
– widok logowania.
Pliki klas mogłyby wyglądać następująco:
- Ekran logowania:
` from app_screens.common.application_screen import ApplicationScreen from app_screens.chats_screen import ChatsScreen from appium.webdriver.common.mobileby import MobileBy as By class LoginScreen(ApplicationScreen): LOGIN_INPUT = (By.ACCESSIBILITY_ID, 'Login') PASSWORD_INPUT = (By.ACCESSIBILITY_ID, 'Password') SUBMIT_BUTTON = (By.ACCESSIBILITY_ID, 'Submit') def __init__(self, driver): super().__init__(driver=driver) def login_to_app(self, login, password, correct=True): self.find_element(self.LOGIN_INPUT).send_keys(login) self.find_element(self.PASSWORD_INPUT).send_keys(password) self.find_element(self.SUBMIT_BUTTON).click() if correct: return ChatsScreen(self.driver) else: return LoginScreen(self.driver) `
- Ekran listy czatów (podobny do listy czatów znanej chociażby z aplikacji Messenger)
` from app_screens.common.application_screen import ApplicationScreen from app_screens.chats_screen from appium.webdriver.common.mobileby import MobileBy as By class ChatsScreen(ApplicationScreen): MESSAGE_AUTHOR = (By.ACCESSIBILITY_ID, 'Message Sender') MESSAGE_CONTENT = (By.ACCESSIBILITY_ID, 'Message Content') MESSAGE_TIME = (By.ACCESSIBILITY_ID, 'Message Time') def __init__(self, driver): super().__init__(driver=driver) def open_chat_with_user(self, user_name): user_locator = (By.XPATH, f'//*[contains(@text, {user_name})]' self.find_element(user_locator).click() return ChatView(self.driver) def get_messages_content(self): self.wait_for_element(self.MESSAGE_CONTENT, timeout=1.5) return [m.text for m in self.find_elements (self.MESSAGE_CONTENT)] `
- ekran czatu:
` from app_screens.common.application_screen import ApplicationScreen from app_screens.chats_screen from appium.webdriver.common.mobileby import MobileBy as By class ChatScreen(ApplicationScreen): INTERLOCUTOR_LABEL = (By.ACCESSIBILITY_ID, 'Interlocutor Name') MESSAGE_CONTENT = (By.ACCESSIBILITY_ID, 'Message Content') MESSAGE_INPUT = (By.ACCESSIBILITY_ID, 'Message Input') SUBMIT_MESSAGE_BUTTON = (By.ACCESSIBILITY_ID, 'Send Message') def __init__(self, driver): super().__init__(driver=driver) def send_message(self, message_content): self.find_element(self.MESSAGE_INPUT).send_keys(message_content) self.find_element(self.SUBMIT_MESSAGE_BUTTON).click() return ChatScreen(self.driver) def get_interlocutor_name(self): return self.find_element(self.INTERLOCUTOR_LABEL).text def get_all_messages_content(self): return [e.text for e in self.find_elements(self.MESSAGE_CONTENT)] `
Jak widać, wszystkie klasy dziedziczą po klasie `ApplicationScreen` – w niej można zawrzeć wszystkie uniwersalne zachowania oraz wrappery, np. własną metodę wyszukującą elementy, zawierającą oczekiwanie na widoczność elementu:
` from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class ApplicationScreen: (...) def find(self, locator, timeout=5): element = WebDriverWait(self.driver, timeout = timeout). until (EC. presence_of_element_located(locator)) return element `
Pomiędzy klasami odpowiadającymi poszczególnym widokom a kodem testów można wprowadzić jeszcze jedną warstwę abstrakcji w której zdefiniowane są częściej używane aktywności — dzięki temu kod testów będzie czytelniejszy, a ich utrzymanie łatwiejsze:
` from app_screens.login_screen import LoginScreen from builders.appium_driver import driver_builder from class Actions: # argumenty poniższej metody zależą od tego, jak wiele parametrów chcemy przekazywać z testu, mogą to być parametry drivera (Appium), typ środowiska etc. def __init__(self, driver): super().__init__(args) self.driver = driver_builder(driver_params) self.current_screen = LoginScreen(self.driver) def login_to_app(login, password, correct=True) self.current_screen = self.current_screen.login_to_app(login,password, correct) def open_chat(user_name): self.current_screen = self.current_screen.open_chat_with_user( user_name) def get_chat_interlocutor(): return self.current_page.get_interlocutor_name() `
Struktura projektu może wtedy wyglądać następująco.
pages |___abstract_page.py |___second_page.py actions |__actions_definitions.py tests |_test_class.py
Dzięki temu, kod testów może być bardzo prosty i czytelny, np. test sprawdzający, czy po otwarciu czatu z użytkownikiem John Smith widzimy odpowiednie dane rozmówcy będzie wyglądał następująco:
` import unitest2 class InterlocutorTest(unitest2.TestCase) from actions import Actions def setUp: # w tym miejscu należy zdefiniować wszystkie działania powtarzalne dla każdej metody - np. stworzenie w bazie danych testowego użytkownika, dodanie czatu etc - wszystko co nie jest przedmiotem naszego testu nie powinno być wykonywane “na froncie”, a np. poprzez API lub bezpośrednio w bazie danych. Jego login i hasło powinniśmy przekazać do metody (login_to_app) self.actions = Actions() self.actions.login_to_app(login, password) def tearDown: # w tym miejscu należy zdefiniować wszystkie działania powtarzalne dla każdej metody, wykonywane na zakończenie testu. Np. wykonanie screenshota w przypadku niepowodzenia i usuwanie danych testowych. def test_see_proper_interlocutor_name(): expected_name = “John Smith” self.actions.open_chat(name) current_name = self.actions.get_chat_interlocutor() self.assertEqual(expected_name, current_name) `
Przygotowanie
Do stworzenia klas odpowiadającym poszczególnym ekranom naszej aplikacji, potrzebujemy lokatorów, które pozwolą Appium na rozpoznanie elementów interface’u i wejście z nimi w interakcje. Przydatnym narzędziem będzie Appium Desktop. Po jego uruchomieniu definiujemy parametry sesji z urządzeniem, na którym jest zainstalowana nasza aplikacja i, korzystając z narzędzia inspektor możemy podejrzeć wszystkie elementy naszej aplikacji.
Appium pozwala na korzystanie ze wszystkich strategii lokatorów, znanych z Selenium, jednak w przypadku aplikacji pisanych w React Native nie będą one pomocne, de facto (jeżeli chcemy mieć uniwersalne lokatory, niezależne od platformy), musimy korzystać z jednej z dwóch strategii:
1. XPATH, odnosząca się do tekstu elementu (i nie zagnieżdżone w roocie dokumentu) np. `//*[@text=”Element’s Text”]` lub `//*[contains(@text, “Element’s Text”)]`
2. ACCESIBILITY_ID, to lokatory odnoszące się do identyfikatorów elementów, ułatwiających korzystanie z aplikacji przez osoby o np. ograniczonym widzeniu.
Korzystanie z ACCESIBILITY_ID, wymaga odpowiedniego przygotowania aplikacji przez zespół programistyczny, jednak pozwala na zlokalizowanie wszystkich typów elementów interface’u aplikacji.
Rys. 3 Inspektor Appium Desktop
Uruchamianie
Przed uruchomieniem testów, uruchamiamy serwer Appium, z którym będziemy się łączyć (czynność tę można oczywiście oskryptować wewnątrz kodu testów, zwłaszcza jeżeli chcemy dynamicznie tworzyć kolejne instancje serwera — chociażby chcąc uruchamiać testy równolegle). Połączenie z serwerem Appium inaugurujemy w kodzie testów, podobnie jak ma to miejsce w przypadku testów Selenium. Z tą różnicą, że inaugurując połączenie musimy podać parametry sesji (Desired Capabilities) — podobnie jak w przypadku łączenia się z desktopową wersją Appium. W Desired Capabilities deklarujemy, na jakiej platformie chcemy uruchamiać testy, na jakim urządzeniu oraz podajemy ścieżkę do pliku aplikacji (*.ipa lub *.apk) — ścieżka ta może mieć także postać URL:
{ "platformName": "iOS", "platformVersion": "11.0", "deviceName": "iPhone 7", "automationName": "XCUITest", "app": "/path/to/my.app" }
Podsumowanie
Appium nie jest narzędziem idealnym, testy wykonywane przy jego użyciu trwają dłużej niż testy przygotowywane w natywnych rozwiązaniach. Jego niewątpliwą zaletą jest możliwość przygotowania jednego kodu testów zarówno dla iOS i Androida — co pozwala na „nadgonienie” przyspieszenia pracy programistów. Jest ono również dobrym wyborem w przypadku, kiedy w zespole znajdują się automatycy posiadający doświadczenie w Selenium — różnice są naprawdę niewielkie (ale mogą dotyczyć bardzo frustrujących aspektów — takich jak kwestia widoczności elementów, która może być inaczej interpretowana na iOS i Androidzie).
Wraz z rosnącą popularnością React Native, na rynku pojawiają się również rozwiązania dedykowane do automatyzacji testów E2E aplikacji napisanych w tym frameworku, jednak, nawet najbardziej zaawansowane — takie jak Detox — nie pozwalają jeszcze na tak szerokie zastosowania jak Appium.