Mobilewright podczas testowania aplikacji Compose Multiplatform

Mobilewright podczas testowania aplikacji Compose Multiplatform
Niedawno otrzymałem zadanie zautomatyzowania testów aplikacji mobilnej napisanej z wykorzystaniem Compose Multiplatform (CMP). Do tego celu wybrano Mobilewright - stosunkowo nowy framework open source inspirowany Playwrightem, przeznaczony do automatyzacji aplikacji na systemy Android oraz iOS. Podczas realizacji projektu poznałem jego możliwości, zalety oraz ograniczenia. W tym artykule dzielę się doświadczeniami z wykorzystania Mobilewright w połączeniu z Compose Multiplatform.

Pierwsze wrażenia

Jeśli wcześniej pracowało się z Playwrightem, poruszanie się po Mobilewright jest bardzo intuicyjne. Wiele koncepcji i rozwiązań działa niemal identycznie. Należą do nich:

  • Locator API,
  • expect,
  • automatyczne oczekiwanie na elementy,
  • przejrzysta struktura testów.

Dzięki temu można szybko rozpocząć pisanie testów, unikając długiego procesu wdrażania się w nowe narzędzie.

Instalacja i konfiguracja 

Rozpoczęcie pracy z Mobilewright jest stosunkowo proste. Framework wystarczy dodać do projektu jako zależność npm, a następnie skonfigurować urządzenie fizyczne lub emulator, na którym będą uruchamiane testy.

Przykładowa instalacja:

$ npm install mobilewright 

Kolejnym krokiem jest przygotowanie pliku konfiguracyjnego, wskazanie platformy testowej oraz określenie sposobu uruchamiania aplikacji. Po poprawnej konfiguracji pierwszy test można uruchomić bezpośrednio z poziomu terminala. 

$ npm run test:android      

Sam proces instalacji i przygotowania środowiska nie sprawia większych problemów, szczególnie, jeśli już wcześniej pracowaliśmy z Playwrightem.

Architektura testów

Podczas pracy nad projektem stopniowo wypracowaliśmy następującą strukturę testów:

spec
 ↓
Screen
 ↓
Selectors

Takie podejście pozwoliło:

  • maksymalnie uprościć testy,
  • przenieść całą logikę obsługi ekranów do klas Screen,
  • wyraźnie oddzielić selektory od logiki biznesowej,
  • znacząco ułatwić utrzymanie testów.

Po refaktoryzacji testy zyskały na czytelności i zaczęły przypominać rzeczywiste scenariusze wykonywane przez użytkownika.

Przykład:

Przykładowy test w Mobilewright po refaktoryzacji: scenariusz rezerwacji waluty podzielony na kroki z użyciem klas Screen (CurrencyScreen, ReservationScreen, MainScreen)

Takie podejście okazało się znacznie praktyczniejsze niż pisanie całej logiki bezpośrednio w plikach spec.

Wyszukiwanie elementów

Mobilewright udostępnia kilka sposobów lokalizowania elementów interfejsu. W wielu przypadkach można korzystać ze standardowych locatorów:

Przykłady locatorów w Mobilewright: getByLabel, getByTestId, getByText, getByType, getByRole, getByPlaceholder – każdy z komentarzem opisującym typ dopasowania
 
W przypadku pracy z Compose Multiplatform najbardziej stabilnym rozwiązaniem okazało się jednak wykorzystanie contentDescription.

Dlatego, aby uzyskać stabilne i łatwe w utrzymaniu selektory, warto już na etapie implementacji aplikacji dodawać identyfikatory do elementów interfejsu.

Przykład:

Fragment kodu Compose: dodanie contentDescription do elementu przez Modifier.semantics – zalecana metoda tworzenia stabilnych selektorów w Mobilewright

Takie podejście znacząco upraszcza tworzenie testów i sprawia, że są one mniej podatne na zmiany w interfejsie. Wsparcie dla automatycznych testów najlepiej uwzględniać już podczas tworzenia aplikacji, zamiast uzupełniać identyfikatory dopiero po rozpoczęciu prac nad testami.

Automatyczne oczekiwanie

Jedną z najbardziej wartościowych funkcji Mobilewright, jest mechanizm automatycznego oczekiwania na elementy. Framework przed wykonaniem akcji sam sprawdza, czy element jest już dostępny i gotowy do interakcji. Dzięki temu w większości przypadków nie ma potrzeby ręcznego implementowania dodatkowych mechanizmów oczekiwania ani stosowania sztucznych opóźnień.

Przykładowo, po przejściu na kolejny ekran nie ma konieczności dodawania własnych pętli sprawdzających dostępność elementów. Wystarczy jedynie wywołać odpowiednią akcję lub asercję, a Mobilewright sam odczeka na gotowość elementu.

Na przykład:

Fragment kodu Mobilewright: automatyczne oczekiwanie na widoczność elementu listy walut przez expect z timeoutem 10 sekund – bez ręcznych pętli oczekiwania
 
Dzięki temu kod testów pozostaje zwięzły i czytelny, a jednocześnie bardziej odporny na niewielkie opóźnienia wynikające z ładowania danych lub animacji interfejsu.

Ograniczenia podczas pracy z Compose Multiplatform

Wsparcie dla iOS

Podczas pracy nad projektem największym ograniczeniem okazało się wsparcie Compose Multiplatform na systemie iOS. Obecnie Mobilewright bardzo dobrze współpracuje z natywnymi aplikacjami Android oraz aplikacjami wykorzystującymi Jetpack Compose. W przypadku Compose Multiplatform dla iOS wsparcie nadal znajduje się w fazie rozwoju, co potwierdza również oficjalna dokumentacja frameworka.

Oznacza to, że niektóre testy da się uruchomić również na iOS, jednak ich stabilność jest wyraźnie niższa niż na Androidzie. Zdarzało się, że ten sam scenariusz testowy przechodził poprawnie kilka razy z rzędu, a następnie kończył się błędem bez wprowadzania jakichkolwiek zmian w aplikacji lub w kodzie testów.

Dodatkowo, nie wszystkie elementy interfejsu Compose Multiplatform są poprawnie udostępniane dla mechanizmów Accessibility wykorzystywanych przez Mobilewright. Powoduje to trudności z lokalizowaniem niektórych elementów oraz wpływa na stabilność testów. Z tego względu na obecnym etapie rozwoju frameworka Mobilewright znacznie lepiej sprawdza się podczas automatyzacji aplikacji Compose Multiplatform na Androidzie niż na iOS. Aktualny poziom wsparcia poszczególnych technologii został przedstawiony w dokumentacji Mobilewright.

Tabela wsparcia frameworków w Mobilewright: pełne wsparcie iOS i Android dla UIKit, SwiftUI, React Native, .NET MAUI; Kotlin Multiplatform działa na Androidzie, iOS w trakcie rozwoju

Brak inspektora drzewa elementów

Jednym z brakujących udogodnień podczas pracy z Mobilewright okazał się pełnoprawny inspektor drzewa elementów.

Mobilewright udostępnia metodę viewTree(), która zwraca informacje o elementach dostępnych dla mechanizmów Accessibility. Każdy element zawiera między innymi jego typ, etykietę (label), tekst (text), współrzędne (bounds) oraz informację o widoczności.

Fragment wyniku metody viewTree() w Mobilewright: lista obiektów z typem, etykietą i współrzędnymi elementów – m.in. title z tekstem 'KURSY WALUT' i currency_item_EUR_PLN
 
Fragment rzeczywistego wyniku metody viewTree() w Mobilewright.

Jak widać na powyższym przykładzie, wynik jest prezentowany w terminalu jako lista obiektów. Pozwala to sprawdzić, jakie elementy są dostępne dla frameworka, jednak nie zapewnia wygodnego, graficznego widoku hierarchii interfejsu.

W konsekwencji analiza złożonych ekranów oraz debugowanie problemów z locatorami jest przez to bardziej czasochłonne. Trudniej też szybko określić, czy element nie został znaleziony z powodu nieprawidłowego locatora, czy dlatego, że nie został poprawnie udostępniony przez aplikację.

Widoczne na zrzucie wartości label, takie jak title lub currency_item_EUR_PLN, odpowiadają właściwościom contentDescription zdefiniowanym w aplikacji Compose Multiplatform.

Praca z polami tekstowymi

Podczas testowania aplikacji Compose Multiplatform napotkałem pewną specyfikę działania metody fill(). Standardowe wywołanie:

await field.fill("new value");

nie zawsze zachowywało się zgodnie z oczekiwaniami. W niektórych polach tekst nie był zastępowany nową wartością – nowy tekst był jedynie dopisywany do już istniejącego. Dodatkowo kursor nie zawsze ustawiał się automatycznie na końcu pola, co utrudniało usuwanie poprzedniej zawartości.

Aby zapewnić stabilność testów, przygotowałem własną pomocniczą metodę czyszczenia pola przed wprowadzeniem nowego ciągu znaków.

Algorytm wygląda następująco:

  1. Odczytanie aktualnej wartości pola.
  2. Ustawienie kursora na końcu tekstu.
  3. Usunięcie wszystkich dotychczasowych znaków.
  4. Wprowadzenie nowej wartości.

Przykładowa implementacja:

Pomocnicza funkcja fillText w Mobilewright: odczytuje bieżącą wartość pola, usuwa znaki backspace po jednym, następnie wpisuje nowy tekst – obejście niestabilnego fill()

Po zastosowaniu takiego rozwiązania praca z polami tekstowymi stała się znacznie bardziej stabilna.

Podsumowanie

Po kilku tygodniach pracy z Mobilewright uważam go za interesujący framework z dużym potencjałem, szczególnie dla osób, które mają już doświadczenie w pracy z Playwrightem.

Do największych zalet zaliczyłbym:

  • proste i czytelne API,
  • niski próg wejścia dla osób znających Playwrighta,
  • wygodny Locator API,
  • automatyczne oczekiwanie na elementy,
  • możliwość zbudowania przejrzystej i łatwej w utrzymaniu architektury testów.

Podczas pracy nad aplikacją Compose Multiplatform napotkałem również pewne ograniczenia. Do najbardziej odczuwalnych należały: niepełne wsparcie systemu iOS, brak wygodnego inspektora drzewa elementów oraz konieczność przygotowania własnego rozwiązania do stabilnej obsługi pól tekstowych.

Mimo tych utrudnień praca z Mobilewright pozwoliła na przygotowanie zestawu testów automatycznych. Jednocześnie zdobyte doświadczenia pokazały, że podczas pracy z Compose Multiplatform warto odpowiednio przygotować aplikację do automatyzacji, między innymi poprzez dodawanie contentDescription do elementów interfejsu.

Przedstawione wnioski odnoszą się do konkretnego przypadku wykorzystania Mobilewright w projekcie Compose Multiplatform. Część opisanych ograniczeń może zostać wyeliminowana w kolejnych wydaniach frameworka.
 

Gdybyś miał/a dziś wdrażać automatyzację dla aplikacji Compose Multiplatform, na co się zdecydujesz?
Gdybyś miał/a dziś wdrażać automatyzację dla aplikacji Compose Multiplatform, na co się zdecydujesz?
50 %
Wykorzystam Mobilewright ze względu na znajomość Playwrighta
50 %
Zostanę przy klasycznym Appium / XCUITest / Espresso
0 %
Poczekam, aż wsparcie dla iOS w nowych frameworkach osiągnie pełną stabilność
0 %
Wybiorę inne narzędzie dedykowane dla Kotlina / Jetpack Compose
0 %
Skupię się wyłącznie na testach jednostkowych i integracyjnych
Łącznie głosów: 4
Źródła:
https://mobilewright.dev/docs
https://github.com/mobile-next/mobilewright
https://kotlinlang.org/docs/multiplatform/compose-accessibility.html

To powinno Cię zainteresować

string(19) "/api/discussions/96" string(3) "GET" string(2) "[]" array(3) { [0]=> string(24) "Accept: application/json" [1]=> string(121) "Authorization: Token dfJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ghJ1aWQiOjEsImV4cCI6MTc5OTk5OTk5OX0.dGhpcy1pcy1hLWZha2Utc2lnbmF" [2]=> string(30) "Content-Type: application/json" }