Compose-Tests werden standardmäßig mit Ihrer Benutzeroberfläche synchronisiert. Wenn Sie eine Assertion oder eine Aktion mit ComposeTestRule aufrufen, wird der Test vorher synchronisiert und wartet, 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 mithilfe einer virtuellen Uhr vorangetrieben. Das bedeutet, dass Compose-Tests nicht in Echtzeit ausgeführt werden, damit sie so schnell wie möglich abgeschlossen werden können.
Wenn Sie jedoch nicht die Methoden verwenden, mit denen Ihre Tests synchronisiert werden, erfolgt keine Neukomposition und die Benutzeroberfläche 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-Benutzeroberfläche synchronisiert. In einigen Fällen möchten Sie diese Synchronisierung möglicherweise beenden und die Uhr selbst steuern. So können Sie beispielsweise den Zeitpunkt für genaue Screenshots einer Animation festlegen, auch wenn die Benutzeroberfläche noch beschäftigt ist. Wenn Sie die automatische Synchronisierung deaktivieren möchten, legen Sie das Attribut autoAdvance in mainClock auf false fest:
composeTestRule.mainClock.autoAdvance = false
Normalerweise stellen Sie die Zeit dann selbst vor. Mit advanceTimeByFrame() kannst du genau ein Frame vorwärts springen und mit advanceTimeBy() um eine bestimmte Zeit:
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Ungenutzte Ressourcen
Compose kann Tests und die Benutzeroberfläche synchronisieren, sodass jede Aktion und Assertion in einem inaktiven Zustand ausgeführt wird. Die Uhr wird bei Bedarf angehalten oder weitergestellt. Einige asynchrone Vorgänge, deren Ergebnisse sich auf den UI-Status auswirken, können jedoch im Hintergrund ausgeführt werden, ohne dass der Test dies bemerkt.
Erstellen und registrieren Sie diese inaktiven Ressourcen in Ihrem Test, damit sie bei der Entscheidung, ob die zu testende App beschäftigt oder inaktiv ist, berücksichtigt werden. Sie müssen nur dann Maßnahmen ergreifen, wenn Sie zusätzliche Leerlaufressourcen registrieren müssen, z. B. wenn Sie einen Hintergrundjob ausführen, der nicht mit Espresso oder Compose synchronisiert wird.
Diese API ähnelt den Idling Resources von Espresso, mit denen angegeben wird, ob das zu testende Element inaktiv ist oder beschäftigt. 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-Benutzeroberfläche mit anderen Teilen Ihres Tests oder der App, die Sie testen, synchronisieren.
Die Funktion waitForIdle() wartet, bis Compose im Leerlauf ist. Die Funktion hängt jedoch von der Eigenschaft autoAdvance 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 Zeichnungs- und Layoutdurchläufe.
Mit advanceTimeUntil() können Sie die Uhr auch so lange vorwärts stellen, 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
Beim Testen von Animationen mit hoher Wiedergabetreue musst du häufig die automatische Wiedergabe deaktivieren und manuell durch die Frames gehen, um Zwischenzustände der Benutzeroberfläche zu bestätigen. Verwende für diese
spezifischen Frame-by-Frame-Schleifen die runWithoutImplicitWait Methode, um
deine Zusicherungen auszuführen. Standardmäßige Knotenabfragen wie onNodeWithTag oder
fetchSemanticsNode lösen implizite Synchronisierungen aus, die redundant sind,
wenn du die Uhr manuell steuerst. Wenn du sie umgehst, werden deine Tests deutlich schneller ausgeführt.
Nutzungsrichtlinien
- Manuelle Uhrverwaltung: Verwende diese API, wenn
mainClock.autoAdvanceauffalsegesetzt ist und sich die Benutzeroberfläche für den aktuellen Frame in einem bekannten, stabilen Zustand befindet. - Ausführung im UI-Thread: Um die Stabilität der UI-Struktur zu gewährleisten, rufe
runWithoutImplicitWaitim UI-Thread auf, z. B. mitrunOnUiThread. Wenn du die Funktion außerhalb des UI-Threads ausführst, ist dein Test von Race-Bedingungen und veralteten Zustandslesevorgängen betroffen. - Schreibgeschützte Zusicherungen: Der Block sollte ausschließlich schreibgeschützte Zusicherungen enthalten. Alle Aktionen, die den Zustand ändern, sollten außerhalb dieses Blocks ausgeführt werden.
Beispiel
@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. Dadurch können Sie waitForIdle und damit auch Compose-UI-Aktionen und ‑Assertions direkt über den Hauptthread aufrufen.
Bisher wurde bei Compose-Tests ein Zwei-Thread-Modell streng durchgesetzt: Die Testausführung erfolgte in einem Hintergrund-Test-Thread, während UI-Aktualisierungen im Hauptthread stattfanden. Wenn Synchronisierungsmethoden wie waitForIdle oder runOnIdle vom Hauptthread aus aufgerufen werden (z. B. innerhalb eines runOnUiThread-Blocks), wird eine IllegalStateException ausgelöst, da das Framework strenge Thread-Prüfungen durchgesetzt hat, um die Synchronisierung des Hauptthreads zu verhindern.
Wenn die Synchronisierung des Hauptthreads aktiviert ist, kann das Compose-Testframework die Zeit voranschreiten lassen 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 sehr vorteilhaft:
- Interoperabilität komplexer Ansichten: Beim Testen von Hybrid-UIs, die sowohl Compose- als auch Legacy-Android-Ansichten enthalten, ist für die Bearbeitung von Ansichten oft die Ausführung im Hauptthread erforderlich. Sie können jetzt sequenziell mit Ansichten interagieren und Compose-Knoten bestätigen, ohne ständig den Thread-Kontext zu wechseln.
- Synchrone Statusänderungen: Wenn Ihre Architektur auf Statusinhabern basiert, die ausschließlich an den Hauptthread gebunden sind, können Sie den Status jetzt ändern und sofort darauf warten, dass die Compose-Benutzeroberfläche sich stabilisiert, ohne den Hauptthread zu verlassen.
- Benutzerdefinierte Test-Runner: Wenn Sie eine benutzerdefinierte Testinfrastruktur erstellen oder Umgebungen verwenden, in denen der Test-Runner standardmäßig im Hauptthread ausgeführt wird, werden Compose-Tests jetzt sauber ausgeführt, ohne dass eine Delegation an den Hintergrundthread erforderlich ist.
Beispiel
Da die Synchronisierung im Hauptthread bisher streng verboten war, mussten Entwickler zwischen dem Hintergrund-Testrunner-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 Zusicherungen 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 Vorgängen abhängen, z. B. vom Laden von Daten oder von Measure oder Draw von Android (d. h. Measure oder Draw 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 Haupt-Landingpage für Android-Tests 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.