I test di Compose vengono sincronizzati per impostazione predefinita con la UI. Quando chiami un'asserzione o un'azione con ComposeTestRule, il test viene sincronizzato in anticipo, attendendo che l'albero della UI sia inattivo.
In genere, non è necessario intraprendere alcuna azione. Tuttavia, esistono alcuni casi limite di cui dovresti essere a conoscenza.
Quando un test viene sincronizzato, l'app Compose viene fatta avanzare nel 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 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 Compose e non al resto dell'app.
Disattiva 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 autonomamente. Ad esempio, puoi controllare il tempo per acquisire screenshot accurati di un'animazione in un punto in cui la UI sarebbe ancora occupata. Per disattivare la sincronizzazione automatica, imposta la proprietà autoAdvance in mainClock su false:
composeTestRule.mainClock.autoAdvance = false
In genere, l'ora viene poi modificata manualmente. Puoi avanzare esattamente di un
frame 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à, attendendo o facendo avanzare l'orologio in base alle esigenze. Tuttavia, alcune operazioni asincrone i cui risultati influiscono sullo stato della UI possono essere eseguite in background senza che il test ne sia a conoscenza.
Crea e registra queste risorse di monitoraggio di inattività nel test in modo che vengano prese in considerazione quando si decide se l'app in fase di test è occupata o inattiva. Non è necessario intervenire, a meno che tu non debba registrare altre risorse di monitoraggio di inattività, ad esempio se esegui un job in background non sincronizzato con Espresso o Compose.
Questa API è molto simile alle risorse di monitoraggio di inattività di Espresso per indicare se l'oggetto sottoposto a test è inattivo o occupato. Utilizza la regola di test di 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 le fasi 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 clock (funziona solo con lo stato di Compose).
Test di animazioni Optimize
Quando testi animazioni ad alta fedeltà, spesso devi disattivare l'avanzamento automatico
e scorrere manualmente i frame per verificare gli stati intermedi della UI. Per questi loop frame per frame specifici, utilizza il metodo runWithoutImplicitWait per eseguire le asserzioni. Le query standard sui nodi (come onNodeWithTag o fetchSemanticsNode) attivano sincronizzazioni implicite che sono ridondanti quando controlli manualmente l'orologio, quindi ignorarle accelera notevolmente i tempi di esecuzione dei test.
Linee guida sull'utilizzo
- Gestione manuale dell'orologio: utilizza questa API quando
mainClock.autoAdvanceè impostato sufalsee l'interfaccia utente è 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 stato obsolete. - Asserzioni di sola lettura: il blocco deve contenere rigorosamente asserzioni di sola lettura. Tutte le 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 della UI di Compose direttamente dal thread principale.
In precedenza, i test di Compose applicavano rigorosamente un modello a due thread: l'esecuzione del 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) generava
un'eccezione IllegalStateException perché il framework applicava controlli rigorosi dei thread
per impedire la sincronizzazione del thread principale.
Con la sincronizzazione del thread principale abilitata, il framework di test di 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 l'esecuzione dei 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à delle visualizzazioni complesse: quando si testano UI ibride contenenti sia Compose sia visualizzazioni Android legacy, la manipolazione delle visualizzazioni spesso richiede l'esecuzione sul thread principale. Ora puoi interagire con le visualizzazioni e fare asserzioni sui nodi di Compose in sequenza senza cambiare costantemente i contesti dei thread.
- Mutazioni di stato sincrone: se la tua architettura si basa su contenitori di stato strettamente associati al thread principale, ora puoi mutare lo stato e attendere immediatamente che Compose UI si stabilizzi senza uscire dal thread principale.
- Esecutori di test personalizzati: se stai creando un'infrastruttura di test personalizzata o utilizzi ambienti in cui l'esecutore di test viene eseguito intrinsecamente sul thread principale, i test di Compose ora vengono eseguiti in modo pulito senza richiedere la delega del thread in background.
Esempio
In passato, poiché la sincronizzazione era severamente vietata nel thread principale, gli sviluppatori dovevano passare continuamente dal thread di esecuzione dei test in background al 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 di 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") } }
Attendi 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
- Testa le app su Android: la pagina di destinazione principale dei test su 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 di base alla base del test di un'app per Android.
- Test locali: puoi eseguire alcuni test localmente, sulla tua workstation.
- Test di instrumentazione: è buona norma eseguire anche i test di instrumentazione. ovvero test eseguiti direttamente sul dispositivo.
- Integrazione continua: l'integrazione continua ti consente di integrare i test nella pipeline di deployment.
- Testa diverse dimensioni dello schermo:con così tanti dispositivi a disposizione degli utenti, devi eseguire test per diverse dimensioni dello schermo.
- Espresso: sebbene sia destinato alle UI basate su View, la conoscenza di Espresso può comunque essere utile per alcuni aspetti dei test di Compose.