Compose-Tests werden standardmäßig mit Ihrer UI synchronisiert. Wenn Sie mit der ComposeTestRule eine
Assertion oder eine Aktion aufrufen, wird der Test vorher
synchronisiert und gewartet, bis der UI-Baum im Leerlauf ist.
Normalerweise müssen Sie nichts weiter tun. Es gibt jedoch einige Grenzfälle, die Sie kennen sollten.
Wenn ein Test synchronisiert wird, wird die Zeit in Ihrer Compose-App mit einer virtuellen Uhr vorverlegt. Das bedeutet, dass Compose-Tests nicht in Echtzeit ausgeführt werden, sodass sie so schnell wie möglich abgeschlossen werden können.
Wenn Sie jedoch nicht die Methoden verwenden, die Ihre Tests synchronisieren, erfolgt keine Neukomposition und die UI scheint pausiert zu sein.
@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()
}Diese Anforderung gilt nur für Compose-Hierarchien und nicht für den Rest der App.
Automatische Synchronisierung deaktivieren
Wenn Sie eine Assertion oder Aktion über die ComposeTestRule aufrufen, z. B. assertExists(), wird Ihr Test mit der Compose-UI synchronisiert. In einigen Fällen möchten Sie diese Synchronisierung möglicherweise beenden und die Uhr selbst steuern. Sie können die Zeit beispielsweise steuern, um genaue Screenshots einer Animation zu einem Zeitpunkt zu erstellen, an dem die UI noch beschäftigt wäre. Wenn Sie die automatische Synchronisierung deaktivieren möchten, legen Sie die Eigenschaft autoAdvance in der mainClock auf false fest:
composeTestRule.mainClock.autoAdvance = false
Normalerweise verlegen Sie die Zeit dann selbst vor. Mit advanceTimeByFrame() können Sie genau ein Frame vorverlegen oder mit advanceTimeBy() eine bestimmte Zeitspanne:
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Inaktive Ressourcen
Compose kann Tests und die UI so synchronisieren, dass jede Aktion und Assertion im Leerlauf ausgeführt wird. Die Uhr wird nach Bedarf angehalten oder vorverlegt. Einige asynchrone Vorgänge, deren Ergebnisse sich auf den UI-Status auswirken, können jedoch im Hintergrund ausgeführt werden, ohne dass der Test davon weiß.
Erstellen und registrieren Sie diese inaktiven Ressourcen in Ihrem Test, damit sie bei der Entscheidung berücksichtigt werden, ob die zu testende App beschäftigt oder inaktiv ist. Sie müssen nichts weiter tun, es sei denn, Sie müssen zusätzliche ungültige Ressourcen registrieren, z. B. wenn Sie einen Hintergrundjob ausführen, der nicht mit Espresso oder Compose synchronisiert wird.
Diese API ähnelt den inaktiven Ressourcen von Espresso, um anzugeben, ob das zu testende Objekt inaktiv oder beschäftigt ist. Verwenden Sie die Compose-Testregel, um
die Implementierung von IdlingResource zu registrieren.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Manuelle Synchronisierung
In bestimmten Fällen müssen Sie die Compose-UI mit anderen Teilen Ihres Tests oder der App synchronisieren, die Sie testen.
Die waitForIdle() Funktion wartet, bis Compose im Leerlauf ist. Die Funktion
hängt jedoch von der autoAdvance Eigenschaft ab:
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.
In beiden Fällen wartet waitForIdle() auch auf ausstehende Zeichen- und Layout
vorgänge.
Außerdem können Sie die Uhr mit
advanceTimeUntil() vorverlegen, bis eine bestimmte Bedingung erfüllt ist.
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
Die angegebene Bedingung sollte den Status prüfen, der von dieser Uhr beeinflusst werden kann. Sie funktioniert nur mit dem Compose-Status.
Animationstests optimieren
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) } } } }
Synchronisierung des Hauptthreads
Compose-Tests unterstützen jetzt die Synchronisierung des Hauptthreads, sodass Sie waitForIdle und damit auch Compose-UI-Aktionen und -Assertions direkt aus dem Hauptthread aufrufen können.
Bisher wurde bei Compose-Tests ein Modell mit zwei Threads verwendet: Die Testausführung erfolgte in einem Hintergrund-Testthread, während UI-Updates im Hauptthread ausgeführt wurden. Wenn Sie Synchronisierungsmethoden wie waitForIdle oder runOnIdle aus dem Hauptthread aufrufen (z. B. in einem runOnUiThread-Block), wird eine IllegalStateException ausgelöst, da das Framework strenge Threadprüfungen durchführte, um die Synchronisierung des Hauptthreads zu verhindern.
Wenn die Synchronisierung des Hauptthreads aktiviert ist, kann das Compose-Testframework die Uhr jetzt vorverlegen und ausstehende Aufgaben verarbeiten, auch wenn blockierende Aufrufe im Hauptthread erfolgen.
Wann sollte die Synchronisierung des Hauptthreads verwendet werden?
Tests im Hintergrundthread sind zwar weiterhin der Standard für reine Compose-Tests, die Synchronisierung des Hauptthreads ist jedoch in einigen bestimmten Szenarien von großem Vorteil:
- Komplexe View-Interoperabilität: Beim Testen von Hybrid-UIs, die sowohl Compose- als auch Legacy-Android-Views enthalten, ist die Bearbeitung von Views oft nur im Hauptthread möglich. Sie können jetzt sequenziell mit Views interagieren und Assertions für Compose-Knoten ausführen, ohne ständig zwischen Threadkontexten wechseln zu müssen.
- Synchrone Statusänderungen: Wenn Ihre Architektur auf Status-Holdern basiert, die ausschließlich an den Hauptthread gebunden sind, können Sie den Status jetzt ändern und sofort warten, bis die Compose-UI im Leerlauf ist, ohne den Hauptthread zu verlassen.
- Benutzerdefinierte Test-Runner: Wenn Sie eine benutzerdefinierte Testinfrastruktur erstellen oder Umgebungen verwenden, in denen der Test-Runner von Natur aus im Hauptthread ausgeführt wird, werden Compose-Tests jetzt sauber ausgeführt, ohne dass eine Delegierung an einen Hintergrundthread erforderlich ist.
Beispiel
Da die Synchronisierung im Hauptthread bisher strengstens verboten war, mussten Entwickler zwischen dem Hintergrund-Test-Runner-Thread und dem UI-Thread hin- und herwechseln, was zu unzusammenhängenden Tests führte:
@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") } }
Wenn die Synchronisierung des Hauptthreads aktiviert ist, können die Assertions für Compose- und View-Hierarchien im selben Block ausgeführt werden:
@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") } }
Auf Bedingungen warten
Für alle Bedingungen, die von externen Aufgaben abhängen, z. B. vom Laden von Daten oder von Android-Messungen oder -Zeichnungen (d. h. Messungen oder Zeichnungen außerhalb von Compose), sollte ein
allgemeineres Konzept wie waitUntil() verwendet werden:
composeTestRule.waitUntil(timeoutMs) { condition }
Sie können auch einen der
waitUntil Helfer verwenden:
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
Zusätzliche Ressourcen
- Apps auf Android testen: Auf der Hauptseite zum Testen von Android-Apps finden Sie einen umfassenderen Überblick über die Grundlagen und Techniken des Testens.
- Grundlagen des Testens: Hier finden Sie weitere Informationen zu den grundlegenden Konzepten für das Testen einer Android-App.
- Lokale Tests: Einige Tests können lokal auf Ihrer Workstation ausgeführt werden.
- Instrumentierte Tests: Es empfiehlt sich, auch instrumentierte Tests auszuführen. Das sind Tests, die direkt auf dem Gerät ausgeführt werden.
- Continuous Integration: Mit Continuous Integration können Sie Ihre Tests in Ihre Bereitstellungspipeline einbinden.
- Verschiedene Bildschirmgrößen testen: Da Nutzer so viele verschiedene Geräte zur Verfügung haben, sollten Sie verschiedene Bildschirmgrößen testen.
- Espresso: Obwohl Espresso für ansichtsbasierte UIs gedacht ist, kann es auch für einige Aspekte von Compose-Tests hilfreich sein.