Stabilność w ramach dużego testu

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 będzie nieaktywny przed następnym działaniem testu lub stwierdzeniem. Jest to synchronizacja.

Synchronizacja testowa

Gdy przeprowadzasz operacje asynchroniczne lub w tle, których nie testujemy, takie jak wczytywanie danych z bazy danych lub wyświetlanie nieskończonych animacji, mogą wystąpić problemy.

Schemat przepływu pokazujący pętlę, która sprawdza, czy aplikacja jest nieaktywna, zanim przejdzie test
Rysunek 1. Testowa synchronizacja

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.

Diagram pokazujący niepowodzenie testu, gdy synchronizacja polega na oczekiwaniu przez określony czas
Ilustracja 2. Wykorzystanie testów w czasie snu prowadzi do spowolnienia testów lub ich niestabilności

Sposoby poprawy stabilności

Duże testy mogą wykrywać wiele regresji jednocześnie, ponieważ testują wiele komponentów aplikacji. Zwykle są 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.

Główne środki zaradcze, które możesz podjąć w celu zmniejszenia efektu niestabilności, to:

  • 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 Automator, pozwalają na szerszy zakres zastosowań, ponieważ umożliwiają interakcję z interfejsem systemu i innymi aplikacjami. Jednak testy UI Automator mogą wymagać bardziej ręcznej synchronizacji, więc są zwykle 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ą one jednak mieć własne dyrektywy konfiguracyjne na potrzeby szczególnych przypadków.

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 przypadku tej konfiguracji poniższe polecenie utworzy obraz emulatora, uruchomi instancję, przeprowadzi testy i wyłączy ją.

./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ę on niestabilny. Te problemy z synchronizacją są głównym źródłem niestabilności, ponieważ struktury testowe muszą wywnioskować, czy działanie jest gotowe, czy też powinno czekać dłużej.

Rozwiązania

Możesz używać zasobów dostępnych 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 kompleksowych. Ponadto zasoby niewykorzystywane mogą być trudne do zainstalowania bez zanieczyszczania testowanego kodu.

Zamiast szacować, czy aktywność jest zajęta, możesz skonfigurować testy tak, aby czekały do spełnienia określonych warunków. 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, aż do ich przejścia.

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 lub 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;
  • powtórzenie 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.