Pisanie automatycznych testów za pomocą Automatora UI

Platforma testowa UI Automator udostępnia zestaw interfejsów API do tworzenia testów interfejsu, które wchodzą w interakcję z aplikacjami użytkownika i aplikacjami systemowymi.

Wprowadzenie do nowoczesnego testowania za pomocą UI Automator

UI Automator 2.4 wprowadza uproszczony, przyjazny dla Kotlina język DSL, który ułatwia pisanie testów interfejsu na Androidzie. Ten nowy interfejs API koncentruje się na wyszukiwaniu elementów na podstawie predykatów i na wyraźnej kontroli stanu aplikacji. Używaj go do tworzenia łatwiejszych w utrzymaniu i bardziej niezawodnych testów automatycznych.

UI Automator umożliwia testowanie aplikacji spoza jej procesu. Dzięki temu możesz testować wersje z zastosowaną minifikacją. UI Automator pomaga też w pisaniu testów Macrobenchmark.

Główne funkcje nowoczesnego podejścia:

  • Dedykowany zakres testowy uiAutomator zapewniający czystszy i bardziej ekspresyjny kod testowy.
  • Metody takie jak onElement, onElements i onElementOrNull do wyszukiwania elementów interfejsu za pomocą jasnych predykatów.
  • Wbudowany mechanizm oczekiwania na elementy warunkowe onElement*(timeoutMs: Long = 10000).
  • Wyraźne zarządzanie stanem aplikacji, np. waitForStable i waitForAppToBeVisible.
  • Bezpośrednia interakcja z węzłami okien ułatwień dostępu w scenariuszach testowania wielu okien.
  • Wbudowane funkcje robienia zrzutów ekranu i ResultsReporter do testowania wizualnego i debugowania.

Konfigurowanie projektu

Aby zacząć korzystać z nowoczesnych interfejsów API UI Automator, zaktualizuj plik projektu build.gradle.kts w celu uwzględnienia najnowszej zależności:

Kotlin

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05")
}

Dynamiczny

dependencies {
  ...
  androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05"
}

Podstawowe koncepcje interfejsu API

W sekcjach poniżej opisujemy podstawowe koncepcje nowoczesnego interfejsu API UI Automator.

Zakres testowy uiAutomator

Dostęp do wszystkich nowych interfejsów API UI Automator uzyskasz w bloku uiAutomator { ... }. Ta funkcja tworzy UiAutomatorTestScope, który zapewnia zwięzłe i bezpieczne środowisko do wykonywania operacji testowych.

uiAutomator {
  // All your UI Automator actions go here
  startApp("com.example.targetapp")
  onElement { textAsString() == "Hello, World!" }.click()
}

Znajdowanie elementów interfejsu

Do lokalizowania elementów interfejsu używaj interfejsów API UI Automator z predykatami. Te predykaty umożliwiają definiowanie warunków dla właściwości takich jak tekst, stan zaznaczenia lub fokus oraz opis treści.

  • onElement { predicate }: zwraca pierwszy element interfejsu, który pasuje do predykatu w domyślnym czasie oczekiwania. Jeśli funkcja nie znajdzie pasującego elementu, zgłosi wyjątek.

    // Find a button with the text "Submit" and click it
    onElement { textAsString() == "Submit" }.click()
    
    // Find a UI element by its resource ID
    onElement { viewIdResourceName == "my_button_id" }.click()
    
    // Allow a permission request
    watchFor(PermissionDialog) {
      clickAllow()
    }
    
  • onElementOrNull { predicate }: podobna do onElement, ale zwraca null jeśli funkcja nie znajdzie pasującego elementu w czasie oczekiwania. Nie zgłasza wyjątku. Używaj tej metody w przypadku elementów opcjonalnych.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: czeka, aż co najmniej 1 element interfejsu będzie pasować do danego predykatu, a następnie zwraca listę wszystkich pasujących elementów interfejsu.

    // Get all items in a list Ui element
    val listItems = onElements { className == "android.widget.TextView" && isClickable }
    listItems.forEach { it.click() }
    

Oto kilka wskazówek dotyczących używania wywołań onElement:

  • Łączenie wywołań onElement w przypadku elementów zagnieżdżonych: możesz łączyć wywołania onElement, aby znajdować elementy w innych elementach, zgodnie z hierarchią nadrzędny-podrzędny.

    // Find a parent Ui element with ID "first", then its child with ID "second",
    // then its grandchild with ID "third", and click it.
    onElement { viewIdResourceName == "first" }
      .onElement { viewIdResourceName == "second" }
      .onElement { viewIdResourceName == "third" }
      .click()
    
  • Określ czas oczekiwania dla funkcji onElement*, przekazując wartość reprezentującą milisekundy.

    // Find a Ui element with a zero timeout (instant check)
    onElement(0) { viewIdResourceName == "something" }.click()
    
    // Find a Ui element with a custom timeout of 10 seconds
    onElement(10_000) { textAsString() == "Long loading text" }.click()
    

Interakcja z elementami interfejsu

Wchodź w interakcję z elementami interfejsu, symulując kliknięcia lub ustawiając tekst w polach edytowalnych.

// Click a Ui element
onElement { textAsString() == "Tap Me" }.click()

// Set text in an editable field
onElement { className == "android.widget.EditText" }.setText("My input text")

// Perform a long click
onElement { contentDescription == "Context Menu" }.longClick()

Obsługa stanów aplikacji i obserwatorów

Zarządzaj cyklem życia aplikacji i obsługuj nieoczekiwane elementy interfejsu, które mogą się pojawić podczas testów.

Zarządzanie cyklem życia aplikacji

Interfejsy API umożliwiają kontrolowanie stanu testowanej aplikacji:

// Start a specific app by package name. Used for benchmarking and other
// self-instrumenting tests.
startApp("com.example.targetapp")

// Start a specific activity within the target app
startActivity(SomeActivity::class.java)

// Start an intent
startIntent(myIntent)

// Clear the app's data (resets it to a fresh state)
clearAppData("com.example.targetapp")

Obsługa nieoczekiwanego interfejsu

Interfejs API watchFor umożliwia definiowanie obsługi nieoczekiwanych elementów interfejsu, takich jak okna z prośbą o uprawnienia, które mogą się pojawić podczas testu. Wykorzystuje on wewnętrzny mechanizm obserwatora, ale zapewnia większą elastyczność.

import androidx.test.uiautomator.PermissionDialog

@Test
fun myTestWithPermissionHandling() = uiAutomator {
  startActivity(MainActivity::class.java)

  // Register a watcher to click "Allow" if a permission dialog appears
  watchFor(PermissionDialog) { clickAllow() }

  // Your test steps that might trigger a permission dialog
  onElement { textAsString() == "Request Permissions" }.click()

  // Example: You can register a different watcher later if needed
  clearAppData("com.example.targetapp")

  // Now deny permissions
  startApp("com.example.targetapp")
  watchFor(PermissionDialog) { clickDeny() }
  onElement { textAsString() == "Request Permissions" }.click()
}

PermissionDialog to przykład ScopedWatcher<T>, gdzie T jest obiektem przekazywanym jako zakres do bloku w watchFor. Na podstawie tego wzorca możesz tworzyć niestandardowe obserwatory.

Oczekiwanie na widoczność i stabilność aplikacji

Czasami testy muszą poczekać, aż elementy staną się widoczne lub stabilne. UI Automator udostępnia kilka interfejsów API, które mogą w tym pomóc.

waitForAppToBeVisible("com.example.targetapp") czeka, aż element interfejsu o podanej nazwie pakietu pojawi się na ekranie w konfigurowalnym czasie oczekiwania.

// Wait for the app to be visible after launching it
startApp("com.example.targetapp")
waitForAppToBeVisible("com.example.targetapp")

Użyj interfejsu API waitForStable(), aby sprawdzić, czy interfejs aplikacji jest stabilny, zanim zaczniesz z nim wchodzić w interakcję.

// Wait for the entire active window to become stable
activeWindow().waitForStable()

// Wait for a specific Ui element to become stable (e.g., after a loading animation)
onElement { viewIdResourceName == "my_loading_indicator" }.waitForStable()

Używanie UI Automator w przypadku Macrobenchmark i profili podstawowych

Używaj UI Automator do testowania wydajności za pomocą Jetpack Macrobenchmark i do generowania profili podstawowych, ponieważ zapewnia on niezawodny sposób interakcji z aplikacją i pomiaru wydajności z perspektywy użytkownika.

Macrobenchmark używa interfejsów API UI Automator do sterowania interfejsem i pomiaru interakcji. Na przykład w testach porównawczych uruchamiania możesz użyć onElement, aby wykryć, kiedy treść interfejsu jest w pełni załadowana, co umożliwia pomiar czasu do pełnego wyświetlenia (TTFD). W testach porównawczych dotyczących zacięć interfejsy API UI Automator służą do przewijania list lub uruchamiania animacji w celu pomiaru czasu trwania klatek. Funkcje takie jak startActivity() czy startIntent() są przydatne do ustawiania aplikacji w odpowiednim stanie przed rozpoczęciem pomiaru.

Podczas generowania profili podstawowych automatyzujesz najważniejsze ścieżki użytkownika w aplikacji, aby rejestrować, które klasy i metody wymagają wstępnej kompilacji. UI Automator to idealne narzędzie do pisania tych skryptów automatyzacji. Wyszukiwanie elementów na podstawie predykatów w nowoczesnym języku DSL i wbudowane mechanizmy oczekiwania (onElement) zapewniają bardziej niezawodne i deterministyczne wykonywanie testów w porównaniu z innymi metodami. Ta stabilność zmniejsza niestabilność i zapewnia, że wygenerowany profil podstawowy dokładnie odzwierciedla ścieżki kodu wykonywane podczas najważniejszych ścieżek użytkownika.

Funkcje zaawansowane

Te funkcje są przydatne w bardziej złożonych scenariuszach testowania.

Interakcja z wieloma oknami

Interfejsy API UI Automator umożliwiają bezpośrednią interakcję z elementami interfejsu i ich sprawdzanie. Jest to szczególnie przydatne w scenariuszach obejmujących wiele okien, takich jak tryb obrazu w obrazie czy układy z podziałem ekranu.

// Find the first window that is in Picture-in-Picture mode
val pipWindow = windows()
  .first { it.isInPictureInPictureMode == true }

// Now you can interact with elements within that specific window
pipWindow.onElement { textAsString() == "Play" }.click()

Zrzuty ekranu i asercje wizualne

Rób zrzuty ekranu całego ekranu, określonych okien lub poszczególnych elementów interfejsu bezpośrednio w testach. Jest to przydatne do testowania regresji wizualnej i debugowania.

uiautomator {
  // Take a screenshot of the entire active window
  val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot()
  fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png"))

  // Take a screenshot of a specific UI element (e.g., a button)
  val buttonBitmap: Bitmap = onElement { viewIdResourceName == "my_button" }.takeScreenshot()
  buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png"))

  // Example: Take a screenshot of a PiP window
  val pipWindowScreenshot = windows()
    .first { it.isInPictureInPictureMode == true }
    .takeScreenshot()
  pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png"))
}

Funkcja rozszerzająca saveToFile dla Bitmap ułatwia zapisywanie przechwyconego obrazu w określonej ścieżce.

Używanie ResultsReporter do debugowania

ResultsReporter ułatwia powiązanie artefaktów testowych, takich jak zrzuty ekranu, bezpośrednio z wynikami testów w Android Studio, co ułatwia sprawdzanie i debugowanie.

uiAutomator {
  startApp("com.example.targetapp")

  val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results
  val file = reporter.addNewFile(
    filename = "my_screenshot",
    title = "Accessible button image" // Title that appears in Android Studio test results
  )

  // Take a screenshot of an element and save it using the reporter
  onElement { textAsString() == "Accessible button" }
    .takeScreenshot()
    .saveToFile(file)

  // Report the artifacts to instrumentation, making them visible in Android Studio
  reporter.reportToInstrumentation()
}

Migracja ze starszych wersji UI Automator

Jeśli masz już testy UI Automator napisane za pomocą starszych interfejsów API, użyj tabeli poniżej jako odniesienia, aby przeprowadzić migrację do nowoczesnego podejścia:

Typ działania Stara metoda UI Automator Nowa metoda UI Automator
Punkt wejścia UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Umieść logikę testu w zakresie uiAutomator { ... }.
Znajdowanie elementów interfejsu device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
Znajdowanie elementów interfejsu device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Oczekiwanie na bezczynny interfejs device.waitForIdle() Zalecamy używanie wbudowanego mechanizmu czasu oczekiwania onElement; w przeciwnym razie użyj activeWindow().waitForStable().
Znajdowanie elementów podrzędnych Ręcznie zagnieżdżone wywołania findObject Łączenie onElement().onElement()
Obsługa okien z prośbą o uprawnienia UiAutomator.registerWatcher() watchFor(PermissionDialog)