I test di Compose vengono sincronizzati per impostazione predefinita con la tua UI. Quando chiami un'asserzione o un'azione con ComposeTestRule, il test viene sincronizzato in anticipo, in attesa che l'albero dell'interfaccia utente sia inattivo.
In genere, non è necessario intraprendere alcuna azione. Tuttavia, ci sono alcuni casi limite di cui dovresti essere a conoscenza.
Quando un test viene sincronizzato, l'app Compose viene avanzata nel tempo utilizzando un orologio virtuale. Ciò significa che i test di composizione 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 si verificherà 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()
}Tieni presente che questo requisito si applica solo alle gerarchie di Composizione e non al resto dell'app.
Disattivare la sincronizzazione automatica
Quando chiami un'asserzione o un'azione tramite ComposeTestRule, ad esempio
assertExists(), il test viene sincronizzato con la UI di Compose. In alcuni casi
potresti voler interrompere questa sincronizzazione e controllare l'orologio manualmente. Ad esempio, puoi controllare il tempo per scattare screenshot accurati di un'animazione in un punto in cui la UI è ancora occupata. Per disattivare la sincronizzazione automatica,
imposta la proprietà autoAdvance in mainClock su false:
composeTestRule.mainClock.autoAdvance = false
In genere, dovrai avanzare l'ora manualmente. Puoi avanzare esattamente di un
fotogramma con advanceTimeByFrame() o di una durata specifica con
advanceTimeBy():
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Risorse inattive
Compose può sincronizzare i test e la UI in modo che ogni azione e asserzione venga eseguita in uno stato di inattività, in attesa o facendo avanzare l'orologio in base alle necessità. 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.
Crea e registra queste risorse inattive nel test in modo che vengano prese in considerazione quando si decide se l'app in fase di test è occupata o inattiva. Non devi intervenire a meno che tu non debba registrare risorse inattive aggiuntive, ad esempio se esegui un job in background che non è sincronizzato con Espresso o Compose.
Questa API è molto simile a Idling Resources di Espresso per indicare se
il soggetto in fase di test è inattivo o occupato. Utilizza la regola di test Compose per registrare
l'implementazione di IdlingResource.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Sincronizzazione manuale
In alcuni casi, devi sincronizzare la UI di Compose con altre parti del test o dell'app che stai testando.
La funzione waitForIdle() attende che Compose sia inattivo, ma la funzione
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() attende anche i passaggi di disegno e layout
in attesa.
Inoltre, puoi far avanzare l'orologio fino a quando non viene soddisfatta una determinata condizione 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 di Compose).
Ottimizzare i test delle animazioni
Quando testi animazioni ad alta fedeltà, spesso devi disattivare l'avanzamento automatico e scorrere manualmente i frame per verificare gli stati intermedi dell'UI. Per questi
loop frame per frame specifici, utilizza il runWithoutImplicitWait metodo per
eseguire le asserzioni. Le query sui nodi standard (come onNodeWithTag o
fetchSemanticsNode) attivano sincronizzazioni implicite ridondanti
quando controlli manualmente l'orologio, quindi ignorarle velocizza notevolmente i tempi di esecuzione dei test.
Linee guida sull'utilizzo
- Gestione manuale dell'orologio: utilizza questa API quando
mainClock.autoAdvanceè impostato sufalsee l'UI è in uno stato noto e stabile per il frame corrente. - Esecuzione del thread dell'interfaccia utente: per garantire la stabilità dell'albero dell'interfaccia utente, chiama
runWithoutImplicitWaitsul thread dell'interfaccia utente, ad esempio conrunOnUiThread. L'esecuzione al di fuori del thread dell'interfaccia utente espone il test a race condition e letture di stati non aggiornati. - Asserzioni di sola lettura: il blocco deve contenere rigorosamente asserzioni di sola lettura. Eventuali azioni che modificano lo stato devono essere eseguite al di fuori di questo blocco.
Esempio
@Test fun runWithoutImplicitWaitSample() = runComposeUiTest { setContent { MainScreen() } mainClock.autoAdvance = false // Trigger an animation onNodeWithText("Start Animation").performClick() // Step through the animation frame-by-frame while (hasPendingWork()) { mainClock.advanceTimeByFrame() waitForIdle() runOnUiThread { // Suppress implicit synchronization inside this block to avoid redundant // waits on each node query, making the frame assertions execute much faster. runWithoutImplicitWait { val box1 = onNodeWithTag("Box1").fetchSemanticsNode() val box2 = onNodeWithTag("Box2").fetchSemanticsNode() val box3 = onNodeWithTag("Box3").fetchSemanticsNode() // Assert the exact intermediate state of all three properties for this frame assert(box1.boundsInRoot.right <= box2.boundsInRoot.left) assert(box2.boundsInRoot.right <= box3.boundsInRoot.left) } } } }
Sincronizzazione del thread principale
I test di Compose ora supportano la sincronizzazione del thread principale, consentendoti di chiamare in sicurezza
waitForIdle e, per estensione, le azioni e le asserzioni dell'interfaccia utente di Compose
direttamente dal thread principale.
In precedenza, i test di Compose applicavano rigorosamente un modello a due thread: l'esecuzione dei test
avveniva su un thread di test in background, mentre gli aggiornamenti della UI avvenivano sul thread
principale. La chiamata di metodi di sincronizzazione come waitForIdle o runOnIdle
dal thread principale (ad esempio, all'interno di un blocco runOnUiThread) genererebbe
un IllegalStateException perché il framework ha applicato controlli rigorosi dei thread
per impedire la sincronizzazione del thread principale.
Con la sincronizzazione del thread principale attivata, il framework di test Compose ora può far avanzare l'orologio ed elaborare il lavoro in attesa anche quando vengono effettuate chiamate di blocco sul thread principale.
Quando utilizzare la sincronizzazione del thread principale
Sebbene mantenere i test sul thread in background rimanga lo standard per i test Compose puri, la sincronizzazione del thread principale è molto vantaggiosa in alcuni scenari specifici:
- Interoperabilità di Complex View: quando si testano UI ibride contenenti sia Compose che View Android legacy, la manipolazione delle View spesso richiede l'esecuzione sul thread principale. Ora puoi interagire con le visualizzazioni e fare asserzioni sui nodi di composizione in sequenza senza cambiare continuamente i contesti dei thread.
- Mutazioni di stato sincrone: se la tua architettura si basa su gestori di stato strettamente associati al thread principale, ora puoi modificare lo stato e attendere immediatamente che la UI di Compose si stabilizzi senza uscire dal thread principale.
- Esecutori di test personalizzati: se stai creando un'infrastruttura di test personalizzata o utilizzando ambienti in cui l'esecutore di test viene eseguito in modo intrinseco sul thread principale, i test Compose ora vengono eseguiti in modo pulito senza richiedere la delega del thread in background.
Esempio
Storicamente, poiché la sincronizzazione era rigorosamente vietata nel thread principale, gli sviluppatori dovevano passare avanti e indietro tra il thread del runner di test in background e il thread dell'interfaccia utente, il che portava a test disgiunti:
@Test fun testBidirectionalInteropUIUpdates_old() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() } // Jump to Test Thread to verify state settles inside compose composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() // Jump back to Main Thread to verify target view state settles scenario.onFragment { fragment -> assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
Con la sincronizzazione del thread principale attivata, le asserzioni per le gerarchie Compose e View possono essere eseguite nello stesso blocco:
@Test fun testBidirectionalInteropUIUpdates_new() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
Attendere le condizioni
Qualsiasi condizione che dipende da un lavoro esterno, come il caricamento dei dati o la misurazione o il disegno di Android (ovvero la misurazione o il disegno esterni a Compose), deve utilizzare un concetto più generale come waitUntil():
composeTestRule.waitUntil(timeoutMs) { condition }
Puoi anche utilizzare uno qualsiasi degli
helper waitUntil:
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
Risorse aggiuntive
- Testare le app su Android: la pagina di destinazione principale dei test Android offre una visione più ampia dei concetti fondamentali e delle tecniche di test.
- Concetti fondamentali sul test di app Android: scopri di più sui concetti fondamentali alla base del test di un'app per Android.
- Test locali: puoi eseguire alcuni test localmente, sulla tua workstation.
- Test strumentati: è buona norma eseguire anche test strumentati. ovvero test eseguiti direttamente sul dispositivo.
- Integrazione continua: l'integrazione continua consente di integrare i test nella pipeline di deployment.
- Testa diverse dimensioni dello schermo: con tutti i dispositivi disponibili per gli utenti, devi eseguire test per diverse dimensioni dello schermo.
- Espresso: sebbene sia destinato alle UI basate sulla visualizzazione, la conoscenza di Espresso può comunque essere utile per alcuni aspetti dei test di Compose.