Per impostazione predefinita, i test di Compose vengono sincronizzati con l'UI. Quando chiami un'
asserzione o un'azione con ComposeTestRule, il test viene sincronizzato
in anticipo, in attesa che l'albero dell'UI 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 Compose non vengono eseguiti in tempo reale, quindi possono essere completati il più rapidamente possibile.
Tuttavia, se non utilizzi i metodi che sincronizzano i test, non si verificherà alcuna ricomposizione e l'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.
Disattivare la sincronizzazione automatica
Quando chiami un'asserzione o un'azione tramite ComposeTestRule, ad esempio assertExists(), il test viene sincronizzato con l'UI di Compose. In alcuni casi, potresti voler interrompere questa sincronizzazione e controllare l'orologio autonomamente. Ad esempio, puoi controllare il tempo per scattare screenshot precisi di un'animazione in un punto in cui l'UI sarebbe ancora occupata. Per disattivare la sincronizzazione automatica, imposta la proprietà autoAdvance in mainClock su false:
composeTestRule.mainClock.autoAdvance = false
In genere, avanzi il tempo autonomamente. 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 l'UI in modo che ogni azione e asserzione venga eseguita in uno stato inattivo, in attesa o avanzando l'orologio in base alle esigenze. Tuttavia, alcune operazioni asincrone i cui risultati influiscono sullo stato dell'UI 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 intraprendere alcuna azione a meno che non sia necessario registrare risorse inattive aggiuntive, ad esempio se esegui un job in background 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 di Compose per registrare
l'implementazione di IdlingResource.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Sincronizzazione manuale
In alcuni casi, devi sincronizzare l'UI di Compose con altre parti del test o dell'app che stai testando.
La waitForIdle() funzione 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 passate di disegno e layout
in attesa.
Inoltre, puoi avanzare l'orologio finché 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 influenzato da questo orologio (funziona solo con lo stato di Compose).
Ottimizzare i test di animazione
When testing high-fidelity animations, you often need to disable auto-advance
and manually step through frames to assert intermediate UI states. For these
specific frame-by-frame loops, use the runWithoutImplicitWait method to
execute your assertions. Standard node queries (like onNodeWithTag or
fetchSemanticsNode) trigger implicit synchronizations that are redundant
when you are manually controlling the clock, so bypassing them significantly
speeds up your test runtimes.
Usage guidelines
- Manual clock management: Use this API when
mainClock.autoAdvanceis set tofalseand the UI is in a known, stable state for the current frame. - UI thread execution: To ensure the stability of the UI tree, call
runWithoutImplicitWaiton the UI thread, such as withrunOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads. - Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.
Example
@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'UI 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 dell'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 IllegalStateException perché il framework applicava controlli rigorosi dei thread per impedire la sincronizzazione del thread principale.
Con la sincronizzazione del thread principale attivata, il framework di test di Compose ora può 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 di Compose puri, la sincronizzazione del thread principale è molto vantaggiosa in alcuni scenari specifici:
- Interoperabilità complessa delle visualizzazioni: quando si testano UI ibride contenenti sia Compose sia le visualizzazioni Android legacy, la manipolazione delle visualizzazioni spesso richiede l'esecuzione su l thread principale. Ora puoi interagire con le visualizzazioni e asserire sui nodi di Compose in sequenza senza cambiare costantemente i contesti dei thread.
- Mutazioni di stato sincrone: se la tua architettura si basa su titolari di stato strettamente associati al thread principale, ora puoi modificare lo stato e attendere immediatamente che l'UI di Compose si stabilizzi senza uscire dal thread principale.
- Runner di test personalizzati: se stai creando un'infrastruttura di test personalizzata o utilizzi ambienti in cui il runner di test viene eseguito in modo intrinseco sul thread principale, i test di Compose ora vengono eseguiti in modo pulito senza richiedere la delega del thread in background.
Esempio
Storicamente, poiché la sincronizzazione era rigorosamente vietata sul 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 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") } }
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 degli
waitUntil helper:
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 di Android fornisce una visione più ampia dei concetti fondamentali e delle tecniche di test.
- Concetti fondamentali sul test: 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 ti consente di integrare i test nella pipeline di deployment.
- Testare diverse dimensioni dello schermo: Con così tanti dispositivi a disposizione degli utenti, dovresti testare 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.