Asynchroniczny charakter aplikacji i ramek mobilnych często utrudnia tworzenie niezawodnych i powtarzalnych testów. Gdy zostanie wstrzyknięte zdarzenie użytkownika, platforma testowa musi poczekać, aż aplikacja zakończy na nie reagowanie. Może to obejmować zmianę tekstu na ekranie lub całkowite odtworzenie aktywności. Gdy test nie ma zachowania deterministycznego, jest niestabilny.
Nowoczesne frameworki, takie jak Compose czy Espresso, są zaprojektowane z myślą o testowaniu, więc istnieje pewność, że interfejs użytkownika będzie nieaktywny przed następną czynnością testową lub stwierdzeniem. Jest to synchronizacja.
Synchronizacja testowa
Problemy mogą wystąpić, gdy wykonujesz operacje asynchroniczne lub w tle, które są nieznane dla testu, np. wczytywanie danych z bazy danych lub wyświetlanie nieskończonych animacji.

Aby zwiększyć niezawodność zestawu testów, możesz zainstalować sposób śledzenia operacji w tle, np. Espresso Idling Resources. Możesz też zastąpić moduły wersjami testowymi, które umożliwiają sprawdzanie bezczynności lub poprawiają synchronizację, np. TestDispatcher w przypadku coroutines lub RxIdler w przypadku RxJava.

Sposoby poprawy stabilności
Duże testy mogą wykrywać wiele regresji jednocześnie, ponieważ testują wiele komponentów aplikacji. Zwykle są one uruchamiane na emulatorach lub urządzeniach, co oznacza, że mają wysoką wierność. Duże kompleksowe testy zapewniają kompleksowe pokrycie, ale są bardziej podatne na sporadyczne błędy.
Oto najważniejsze działania, które możesz podjąć, aby ograniczyć niestabilność:
- Prawidłowo skonfiguruj urządzenia
- Zapobieganie problemom z synchronizacją
- Wdrażanie ponownych prób
Aby utworzyć duże testy za pomocą Compose lub Espresso, zazwyczaj uruchamiasz jedną z działalności i przeglądasz ją tak, jak użytkownik, sprawdzając, czy interfejs użytkownika działa prawidłowo, za pomocą stwierdzeń lub testów polegających na tworzeniu zrzutów ekranu.
Inne platformy, takie jak UI Automation, umożliwiają szerszy zakres działania, ponieważ możesz wchodzić w interakcję z interfejsem systemu i innymi aplikacjami. Testy automatyzacji interfejsu użytkownika mogą jednak wymagać ręcznej synchronizacji, przez co są mniej niezawodne.
Konfigurowanie urządzeń
Aby zwiększyć niezawodność testów, najpierw sprawdź, czy system operacyjny urządzenia nie przerywa nieoczekiwanie wykonywania testów. Na przykład gdy okno aktualizacji systemu wyświetla się nad innymi aplikacjami lub gdy na dysku jest za mało miejsca.
Dostawcy farmy urządzeń konfigurują swoje urządzenia i emulatory, więc zazwyczaj nie musisz podejmować żadnych działań. Mogą jednak mieć własne wskazówki dotyczące konfiguracji w szczególnych przypadkach.
Urządzenia zarządzane przez Gradle
Jeśli sam zarządzasz emulacją, możesz użyć urządzeń zarządzanych przez Gradle, aby określić, których urządzeń używać do uruchamiania testów:
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
// Use device profiles you typically see in Android Studio.
device = "Pixel 2"
// Use only API levels 27 and higher.
apiLevel = 30
// To include Google services, use "google".
systemImageSource = "aosp"
}
}
}
}
}
W tej konfiguracji to polecenie utworzy obraz emulatora, uruchomi instancję, przeprowadzi testy i ją zamknie.
./gradlew pixel2api30DebugAndroidTest
Urządzenia zarządzane przez Gradle zawierają mechanizmy ponownego próbowania w przypadku utraty połączenia z urządzeniem oraz inne ulepszenia.
Zapobieganie problemom z synchronizacją
Komponenty wykonujące operacje w tle lub asynchroniczne mogą powodować niepowodzenia testów, ponieważ instrukcja testu została wykonana, zanim interfejs użytkownika był gotowy. Wraz ze wzrostem zakresu testu rośnie ryzyko, że stanie się niestabilny. Te problemy z synchronizacją są głównym źródłem niestabilności, ponieważ frameworki testów muszą wywnioskować, czy aktywność została załadowana, czy też powinna poczekać dłużej.
Rozwiązania
Możesz używać zasobów do śledzenia nieaktywności w Espresso, aby określić, kiedy aplikacja jest zajęta, ale trudno jest śledzić każdą asynchroniczną operację, zwłaszcza w przypadku bardzo dużych testów end-to-end. Ponadto zasoby niewykorzystywane mogą być trudne do zainstalowania bez zanieczyszczania testowanego kodu.
Zamiast szacować, czy dana aktywność jest zajęta, możesz kazać testom czekać, aż zostaną spełnione określone warunki. Możesz na przykład poczekać, aż w interfejsie pojawi się określony tekst lub element.

Compose zawiera zbiór interfejsów API do testowania, które umożliwiają:
ComposeTestRule
oczekywanie na różne dopasowywacze:
fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)
Ogólny interfejs API, który przyjmuje dowolną funkcję zwracającą wartość logiczną:
fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit
Przykład użycia:
composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>
Mechanizmy ponawiania
Należy naprawić testy niestabilne, ale czasami warunki, które powodują ich niepowodzenie, są tak nieprawdopodobne, że trudno je odtworzyć. Chociaż zawsze należy śledzić i naprawiać niestabilne testy, mechanizm ponownego próbowania może pomóc deweloperom w zachowaniu produktywności, ponieważ uruchamia testy wielokrotnie, dopóki nie zostaną zaliczone.
Ponowne próby muszą się odbywać na wielu poziomach, aby uniknąć problemów takich jak:
- Przekroczono limit czasu połączenia z urządzeniem lub połączenie zostało utracone
- Pojedynczy błąd testu
Instalowanie i konfigurowanie prób ponownego wykonania zależy od używanych frameworków i infrastruktury testowej, ale typowe mechanizmy to:
- reguła JUnit, która powtarza dowolny test określoną liczbę razy;
- ponowne wykonanie działania lub kroku w Twoim przepływie pracy CI.
- System do ponownego uruchamiania emulatora, gdy przestaje reagować, np. urządzenia zarządzane przez Gradle.