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.
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.
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
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Semantica nella scrittura
- Interni di finestre in Scrivi
- Altre considerazioni