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 Automatora

UI Automator 2.4 wprowadza uproszczony język DSL (Domain Specific Language) przyjazny dla języka Kotlin, który ułatwia pisanie testów interfejsu na Androida. Ten nowy interfejs API skupia się na wyszukiwaniu elementów na podstawie predykatów i jawnej kontroli nad stanami aplikacji. Używaj go do tworzenia łatwiejszych w utrzymaniu i bardziej niezawodnych testów automatycznych.

UI Automator umożliwia testowanie aplikacji z zewnątrz jej procesu. Dzięki temu możesz testować wersje do publikacji z zastosowaną minimalizacją. UI Automator pomaga też w pisaniu testów makrobenchmarkowych.

Najważniejsze cechy nowoczesnego podejścia to:

  • Dedykowany uiAutomator zakres testowy, który zapewnia bardziej przejrzysty i wyrazisty kod testowy.
  • Metody takie jak onElement, onElementsonElementOrNull do znajdowania elementów interfejsu z jasnymi predykatami.
  • Wbudowany mechanizm oczekiwania na elementy warunkowe onElement*(timeoutMs: Long = 10000)
  • Jawne zarządzanie stanem aplikacji, np. waitForStablewaitForAppToBeVisible.
  • Bezpośrednia interakcja z węzłami okna 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 build.gradle.kts projektu, aby zawierał najnowszą zależność:

Kotlin

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

Groovy

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

Podstawowe pojęcia związane z interfejsem API

W sekcjach poniżej opisujemy podstawowe pojęcia związane z nowoczesnym interfejsem UI Automator API.

Zakres testu uiAutomator

Dostęp do wszystkich nowych interfejsów API UI Automator w bloku uiAutomator { ... }. Ta funkcja tworzy UiAutomatorTestScope, które zapewnia zwięzłe i bezpieczne pod względem typów środowisko dla operacji testowych.

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

Znajdowanie elementów interfejsu

Używaj interfejsów UI Automator API z predykatami, aby lokalizować elementy interfejsu. Te predykaty umożliwiają określanie 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 { id == "my_button_id" }.click()
    
    // Allow a permission request
    watchFor(PermissionDialog) {
      clickAllow()
    }
    
  • onElementOrNull { predicate }: Podobna do funkcji onElement, ale zwraca wartość null, jeśli w określonym czasie nie znajdzie pasującego elementu. 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 użytkownika będzie pasować do podanego predykatu, a następnie zwraca listę wszystkich pasujących elementów interfejsu użytkownika.

    // 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 korzystania z połączeń onElement:

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

    // Find a parent Ui element with ID "first", then its child with ID "second",
    // then its grandchild with ID "third", and click it.
    onElement { id == "first" }
      .onElement { id == "second" }
      .onElement { id == "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) { id == "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 wpisują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ą pojawić się 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 watchFor API umożliwia definiowanie procedur obsługi nieoczekiwanych elementów interfejsu, takich jak okna dialogowe z prośbą o uprawnienia, które mogą pojawić się podczas testu. Korzysta on z wewnętrznego mechanizmu 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 to obiekt przekazywany jako zakres do bloku w watchFor. Na podstawie tego wzorca możesz tworzyć niestandardowe obserwatory.

Poczekaj 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 na pojawienie się na ekranie elementu interfejsu o podanej nazwie pakietu 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 waitForStable() API, aby przed interakcją z interfejsem aplikacji sprawdzić, czy jest on stabilny.

// 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 { id == "my_loading_indicator" }.waitForStable()

Funkcje zaawansowane

Poniższe funkcje są przydatne w bardziej złożonych scenariuszach testowych.

Interakcja z wieloma oknami

Interfejsy API UI Automator umożliwiają bezpośrednie interakcje z elementami interfejsu i ich sprawdzanie. Jest to szczególnie przydatne w scenariuszach obejmujących wiele okien, takich jak tryb obrazu w obrazie lub układy z podzielonym ekranem.

// 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 całego ekranu, konkretnych okien lub poszczególnych elementów interfejsu bezpośrednio w testach. Jest to przydatne w przypadku 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 { id == "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 rozszerzenia saveToFile dla mapy bitowej upraszcza zapisywanie przechwyconego obrazu w określonej ścieżce.

Używanie ResultsReporter do debugowania

ResultsReporter pomaga powiązać artefakty testowe, takie 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 Automatora

Jeśli masz już testy UI Automator napisane przy użyciu starszych interfejsów API, skorzystaj z tej tabeli, aby przenieść je na nowoczesne podejście:

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 { id == "my\_button" }
Znajdowanie elementów interfejsu device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Czekaj na bezczynny interfejs device.waitForIdle() Preferuj wbudowany mechanizm limitu czasu onElement; w przeciwnym razie activeWindow().waitForStable()
Znajdowanie elementów podrzędnych Ręcznie zagnieżdżone wywołania findObject onElement().onElement() łączenie
Obsługa okien z prośbą o uprawnienia UiAutomator.registerWatcher() watchFor(PermissionDialog)