Test del layout di Scrivi

Il test di UI o schermate consente di verificare il comportamento corretto del codice di scrittura, migliorando la qualità della tua app rilevando gli errori nelle prime fasi del processo di sviluppo.

Compose fornisce un insieme di API di test per trovare elementi, verificare i loro attributi ed eseguire azioni utente. Includono anche funzionalità avanzate come la gestione dell'ora.

Semantica

I test dell'interfaccia utente in Compose utilizzano la semantica per interagire con la gerarchia dell'interfaccia utente. La semantica, come suggerisce il nome, dà significato a un elemento dell'interfaccia utente. In questo contesto, una "parte dell'interfaccia utente" (o elemento) può indicare qualsiasi elemento, da una singola componibile a uno schermo intero. L'albero della semantica viene generato insieme alla gerarchia dell'interfaccia utente e la descrive.

Diagramma che mostra un layout tipico dell'interfaccia utente e il modo in cui quest'ultimo viene mappato a un albero semantico corrispondente

Figura 1. Una tipica gerarchia dell'interfaccia utente e la relativa semantica.

Il framework semantica è utilizzato principalmente per l'accessibilità, quindi i test sfruttano le informazioni esposte dalla semantica sulla gerarchia dell'interfaccia utente. Sono gli sviluppatori a decidere cosa e quanto esporre.

Un pulsante contenente grafica e testo

Figura 2. Tipico pulsante contenente un'icona e del testo.

Ad esempio, dato un pulsante come questo che consiste in un'icona e un elemento di testo, l'albero della semantica predefinita contiene solo l'etichetta di testo "Mi piace". Questo perché alcuni componenti componibili, come Text, espongono già alcune proprietà all'albero della semantica. Puoi aggiungere proprietà all'albero semantico utilizzando un Modifier.

MyButton(
    modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)

Configurazione

Questa sezione descrive come configurare il modulo per testare il codice di scrittura.

Innanzitutto, aggiungi le seguenti dipendenze al file build.gradle del modulo che contiene i tuoi test dell'interfaccia utente:

// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Questo modulo include un ComposeTestRule e un'implementazione per Android chiamata AndroidComposeTestRule. Con questa regola puoi impostare i contenuti di Scrivi contenuti o accedere all'attività. Il tipico test dell'interfaccia utente per Scrivi ha il seguente aspetto:

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    // use createAndroidComposeRule<YourActivity>() if you need access to
    // an activity

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = fakeUiState, /*...*/)
            }
        }

        composeTestRule.onNodeWithText("Continue").performClick()

        composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
    }
}

Test delle API

Esistono tre modi principali per interagire con gli elementi:

  • I Trovatori ti consentono di selezionare uno o più elementi (o nodi nell'albero semantico) per creare asserzioni o eseguire azioni su di essi.
  • Le asserzioni vengono utilizzate per verificare che gli elementi esistano o abbiano determinati attributi.
  • Le azioni inseriscono eventi utente simulati sugli elementi, come clic o altri gesti.

Alcune di queste API accettano un elemento SemanticsMatcher per fare riferimento a uno o più nodi nell'albero della semantica.

Ricercatori

Puoi utilizzare onNode e onAllNodes per selezionare rispettivamente uno o più nodi, ma puoi anche utilizzare dei convenienti per le ricerche più comuni, ad esempio onNodeWithText , onNodeWithContentDescription e così via. Puoi sfogliare l'elenco completo nella scheda di riferimento per il test di Scrivi.

Seleziona un singolo nodo

composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
    .onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

Seleziona più nodi

composeTestRule
    .onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
    .onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

Utilizzo dell'albero non unito

Alcuni nodi uniscono le informazioni sulla semantica dei rispettivi nodi. Ad esempio, un pulsante con due elementi di testo unisce le relative etichette:

MyButton {
    Text("Hello")
    Text("World")
}

Da un test, possiamo utilizzare printToLog() per mostrare l'albero della semantica:

composeTestRule.onRoot().printToLog("TAG")

Questo codice stampa il seguente output:

Node #1 at (...)px
 |-Node #2 at (...)px
   Role = 'Button'
   Text = '[Hello, World]'
   Actions = [OnClick, GetTextLayoutResult]
   MergeDescendants = 'true'

Se devi associare un nodo di quello che sarebbe l'albero non unito, puoi impostare useUnmergedTree su true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

Questo codice stampa il seguente output:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = '[Hello]'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = '[World]'

Il parametro useUnmergedTree è disponibile in tutti i strumenti di ricerca. Ad esempio, qui è utilizzato in un ricerca di onNodeWithText.

composeTestRule
    .onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

Asserzioni

Controlla le asserzioni chiamando assert() sulla SemanticsNodeInteraction restituita da chi cerca uno o più matcher:

// Single matcher:
composeTestRule
    .onNode(matcher)
    .assert(hasText("Button")) // hasText is a SemanticsMatcher

// Multiple matchers can use and / or
composeTestRule
    .onNode(matcher).assert(hasText("Button") or hasText("Button2"))

Puoi anche utilizzare funzioni di convenienza per le asserzioni più comuni, come assertExists , assertIsDisplayed , assertTextEquals e così via. Puoi sfogliare l'elenco completo nella scheda di riferimento per il test della scrittura.

Esistono anche funzioni per verificare le asserzioni su una raccolta di nodi:

// Check number of matched nodes
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())

Azioni

Per inserire un'azione su un nodo, chiama una funzione perform…():

composeTestRule.onNode(...).performClick()

Ecco alcuni esempi di azioni:

performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }

Puoi sfogliare l'elenco completo nella scheda di riferimento per il test di Scrivi.

Partite

Questa sezione descrive alcuni dei matcher disponibili per testare il codice di Scrivi.

Matcher gerarchici

I matcher gerarchici consentono di aumentare o diminuire la semantica ed eseguire semplici corrispondenze.

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

Di seguito sono riportati alcuni esempi di utilizzo di questi matcher:

composeTestRule.onNode(hasParent(hasText("Button")))
    .assertIsDisplayed()

Selettori

Un modo alternativo per creare i test consiste nell'utilizzare dei selettori, che possono rendere più leggibili alcuni test.

composeTestRule.onNode(hasTestTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

Puoi sfogliare l'elenco completo nella scheda di riferimento per il test di Scrivi.

Sincronizzazione

I test di scrittura sono sincronizzati per impostazione predefinita con la tua UI. Quando chiami un'asserzione o un'azione tramite ComposeTestRule, il test viene sincronizzato in anticipo, attendendo che la struttura dell'interfaccia utente sia inattiva.

In genere, non è richiesta alcuna azione da parte tua. Tuttavia, ci sono alcuni casi limite che dovresti conoscere.

Quando un test viene sincronizzato, l'app Compose viene avanzata in tempo utilizzando un orologio virtuale. Ciò significa che i test di Compose non vengono eseguiti in tempo reale, quindi possono essere superati il più rapidamente possibile.

Tuttavia, se non utilizzi i metodi che sincronizzano i test, non verrà eseguita alcuna ricomposizione e la UI sembrerà in pausa.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions
    var lastSeenValue = 0 // Used to track recompositions
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition

    // Fails because nothing triggered a recomposition
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition
    composeTestRule.onNodeWithText("1").assertExists()
}

È inoltre importante tenere presente che questo requisito si applica solo alle gerarchie di Scrivi e non al resto dell'app.

Disattivazione della sincronizzazione automatica

Quando chiami un'asserzione o un'azione tramite ComposeTestRule, come assertExists(), il tuo test viene sincronizzato con l'UI di composizione. In alcuni casi è possibile interrompere la sincronizzazione e controllare personalmente l'orologio. Ad esempio, puoi controllare il tempo per acquisire screenshot accurati di un'animazione in un punto in cui l'interfaccia utente sarebbe ancora occupata. Per disattivare la sincronizzazione automatica, imposta la proprietà autoAdvance in mainClock su false:

composeTestRule.mainClock.autoAdvance = false

In genere puoi avanzare di nuovo il tempo personalmente. Puoi avanzare esattamente di un frame con advanceTimeByFrame() o di una durata specifica con advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Risorse inattive

La scrittura può sincronizzare i test e l'interfaccia utente in modo che ogni azione e asserzione venga eseguita in stato inattivo, in attesa o avanzando l'orologio a seconda delle esigenze. Tuttavia, alcune operazioni asincrone i cui risultati influiscono sullo stato dell'interfaccia utente possono essere eseguite in background mentre il test non ne è a conoscenza.

Puoi creare e registrare queste risorse di inattività nel test in modo che vengano prese in considerazione per stabilire se l'app sottoposta a test è occupata o inattiva. Non devi intraprendere alcuna azione, a meno che tu non debba registrare risorse inattiva aggiuntive, ad esempio se esegui un job in background non sincronizzato con Espresso o Compose.

Questa API è molto simile alle Risorse inattive di Espresso per indicare se il soggetto sottoposto a test è inattivo o occupato. Puoi utilizzare la regola di test Scrivi per registrare l'implementazione di IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Sincronizzazione manuale

In alcuni casi, devi sincronizzare l'interfaccia utente di Scrivi con altre parti del test o dell'app che stai testando.

waitForIdle attende che Compose sia inattivo, ma dipende dalla proprietà autoAdvance:

composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle

Tieni presente che in entrambi i casi, waitForIdle attenderà anche i passaggi di disegno e layout in attesa.

Inoltre, puoi avanzare l'orologio fino a quando una determinata condizione non viene soddisfatta con advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Tieni presente che la condizione specificata deve controllare lo stato che può essere interessato da questo orologio (funziona solo con lo stato Scrivi).

In attesa delle condizioni

Qualsiasi condizione che dipende da un lavoro esterno, come il caricamento dei dati o la misura o il disegno di Android (ovvero misurazione o disegno esterno a Compose), dovrebbe utilizzare un concetto più generale come waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Puoi anche utilizzare uno degli helper waitUntil:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Pattern comuni

Questa sezione descrive alcuni approcci comuni che noterai nel test di Scrivi.

Test in isolamento

ComposeTestRule ti consente di avviare un'attività visualizzando qualsiasi elemento componibile: l'applicazione completa, una singola schermata o un piccolo elemento. È buona norma anche verificare che i componenti componibili siano incapsulati correttamente e funzionino in modo indipendente, consentendo test dell'interfaccia utente più semplici e mirati.

Questo non significa che devi creare solo test UI delle unità. Sono molto importanti anche i test che interessano parti più ampie dell'interfaccia utente.

Accedi all'attività e alle risorse dopo aver impostato i tuoi contenuti

Spesso è necessario impostare i contenuti in fase di test utilizzando composeTestRule.setContent e anche accedere alle risorse di attività, ad esempio per affermare che un testo visualizzato corrisponde a una risorsa stringa. Tuttavia, non puoi chiamare setContent per una regola creata con createAndroidComposeRule() se l'attività la chiama già.

Un pattern comune per raggiungere questo obiettivo è creare un AndroidComposeTestRule utilizzando un'attività vuota, ad esempio ComponentActivity.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, /*...*/)
            }
        }
        val continueLabel = composeTestRule.activity.getString(R.string.next)
        composeTestRule.onNodeWithText(continueLabel).performClick()
    }
}

Tieni presente che ComponentActivity deve essere aggiunto al file AndroidManifest.xml dell'app. Puoi farlo aggiungendo questa dipendenza al modulo:

debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Proprietà della semantica personalizzata

Puoi creare proprietà della semantica personalizzate per esporre le informazioni ai test. Per farlo, definisci un nuovo SemanticsPropertyKey e rendilo disponibile utilizzando SemanticsPropertyReceiver.

// Creates a Semantics property of type Long
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey

Ora puoi utilizzare questa proprietà utilizzando il modificatore semantics:

val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
    modifier = Modifier.semantics { pickedDate = datePickerValue }
)

Dai test, puoi utilizzare SemanticsMatcher.expectValue per rivendicare il valore della proprietà:

composeTestRule
    .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
    .assertExists()

Verifica il ripristino dello stato

Devi verificare che lo stato degli elementi Compose venga ripristinato correttamente quando vengono ricreati l'attività o il processo. È possibile eseguire questo controllo senza fare affidamento sulla ricreazione dell'attività con la classe StateRestorationTester.

Questa classe consente di simulare la ricreazione di un componibile. È particolarmente utile verificare l'implementazione di rememberSaveable.


class MyStateRestorationTests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun onRecreation_stateIsRestored() {
        val restorationTester = StateRestorationTester(composeTestRule)

        restorationTester.setContent { MainScreen() }

        // TODO: Run actions that modify the state

        // Trigger a recreation
        restorationTester.emulateSavedInstanceStateRestore()

        // TODO: Verify that state has been correctly restored.
    }
}

Debug

Il modo principale per risolvere i problemi nei test è osservare l'albero della semantica. Puoi stampare la struttura ad albero chiamando composeTestRule.onRoot().printToLog() in qualsiasi momento del test. Questa funzione stampa un log come il seguente:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hi'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'There'

Questi log contengono informazioni preziose per tenere traccia dei bug.

Interoperabilità con il caffè espresso

In un'app ibrida puoi trovare i componenti di Scrivi all'interno delle gerarchie e delle viste di visualizzazione all'interno dei componenti componibili di Compose (tramite l'elemento componibile AndroidView).

Non sono necessari passaggi speciali per la corrispondenza con nessuno dei due tipi. Puoi abbinare le viste tramite gli elementi onView di Espresso e Compose tramite ComposeTestRule.

@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state:
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}

Interoperabilità con UiAutomator

Per impostazione predefinita, i componibili sono accessibili da UiAutomator solo tramite i loro pratici descrittori (testo visualizzato, descrizione dei contenuti e così via). Se vuoi accedere a qualsiasi elemento componibile che utilizza Modifier.testTag, devi abilitare la proprietà semantica testTagAsResourceId per il sottoalbero dei componibili specifico. L'attivazione di questo comportamento è utile per gli elementi componibili che non hanno altri handle univoci, come gli elementi componibili scorrevoli (ad esempio LazyColumn).

Puoi abilitarla solo una volta in cima alla gerarchia dei componibili per assicurarti che tutti i componibili nidificati con Modifier.testTag siano accessibili da UiAutomator.

Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // content
    }
}

Qualsiasi componibile con Modifier.testTag(tag) può essere accessibile con l'utilizzo di By.res(resourceName) utilizzando lo stesso tag di resourceName.

val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn

Scopri di più

Per scoprire di più, prova il codelab sul test di Jetpack Compose.

Samples