Tests synchronisieren

Compose-Tests werden standardmäßig mit Ihrer Benutzeroberfläche synchronisiert. Wenn Sie eine Assertion oder eine Aktion mit dem ComposeTestRule aufrufen, wird der Test vorher synchronisiert und wartet, bis die UI-Struktur inaktiv ist.

Normalerweise müssen Sie nichts weiter tun. Es gibt jedoch einige Grenzfälle, die Sie kennen sollten.

Wenn ein Test synchronisiert wird, wird Ihre Compose-App mithilfe einer virtuellen Uhr in der Zeit vorgerückt. Das bedeutet, dass Compose-Tests nicht in Echtzeit ausgeführt werden, sodass sie so schnell wie möglich bestanden werden können.

Wenn Sie jedoch nicht die Methoden verwenden, die Ihre Tests synchronisieren, wird keine Neuzusammensetzung durchgeführt und die Benutzeroberfläche wird pausiert.

@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 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 beispielsweise die Zeit steuern, um genaue Screenshots einer Animation an einem Punkt zu erstellen, an dem die Benutzeroberfläche noch beschäftigt ist. Wenn Sie die automatische Synchronisierung deaktivieren möchten, setzen Sie das Attribut autoAdvance in der mainClock auf false:

composeTestRule.mainClock.autoAdvance = false

Normalerweise stellen Sie die Zeit dann selbst ein. Sie können mit advanceTimeByFrame() genau einen Frame oder mit advanceTimeBy() eine bestimmte Dauer vorspringen:

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Ungenutzte Ressourcen

Compose kann Tests und die Benutzeroberfläche synchronisieren, sodass jede Aktion und Assertion im inaktiven Zustand ausgeführt wird. Dabei wird die Uhr nach Bedarf angehalten oder vorgestellt. 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 inaktive Ressourcen registrieren, z. B. wenn Sie einen Hintergrundjob ausführen, der nicht mit Espresso oder Compose synchronisiert ist.

Diese API ist den inaktiven Ressourcen von Espresso sehr ähnlich und gibt an, ob das Testobjekt 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 zu testenden App synchronisieren.

Die Funktion waitForIdle() wartet, bis Compose inaktiv ist. Die Funktion hängt jedoch vom Attribut 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 Zeichen- und Layoutdurchgänge.

Außerdem können Sie die Uhr mit advanceTimeUntil() so lange vorstellen, 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 High-Fidelity-Animationen müssen Sie oft den automatischen Übergang deaktivieren und manuell durch die Frames gehen, um Zwischenzustände der Benutzeroberfläche zu prüfen. Verwenden Sie für diese spezifischen Frame-für-Frame-Schleifen die Methode runWithoutImplicitWait, um Ihre Assertions auszuführen. Standardknotenabfragen (wie onNodeWithTag oder fetchSemanticsNode) lösen implizite Synchronisierungen aus, die redundant sind, wenn Sie die Uhr manuell steuern. Wenn Sie sie umgehen, werden die Testlaufzeiten erheblich beschleunigt.

Nutzungsrichtlinien

  • Manuelle Taktverwaltung: Verwenden Sie diese API, wenn mainClock.autoAdvance auf false gesetzt 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 des UI-Baums zu gewährleisten, rufen Sie runWithoutImplicitWait im UI-Thread auf, z. B. mit runOnUiThread. Wenn Sie ihn außerhalb des UI-Threads ausführen, ist Ihr Test Race-Bedingungen und veralteten Statuslesevorgängen ausgesetzt.
  • Schreibgeschützte Assertions: Der Block darf nur schreibgeschützte Assertions enthalten. Alle Aktionen, die den Status ä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, sodass Sie waitForIdle und damit auch Compose-UI-Aktionen und -Assertions sicher direkt über den Hauptthread aufrufen können.

Bisher wurde beim Testen von Compose strikt ein Zwei-Thread-Modell erzwungen: Die Testausführung erfolgte in einem Hintergrund-Test-Thread, während UI-Updates im Hauptthread stattfanden. Wenn Synchronisierungsmethoden wie waitForIdle oder runOnIdle vom Hauptthread aufgerufen werden (z. B. innerhalb eines runOnUiThread-Blocks), wird ein IllegalStateException ausgelöst, da das Framework strenge Threadprüfungen erzwingt, um die Synchronisierung des Hauptthreads zu verhindern.

Wenn die Synchronisierung des Hauptthreads aktiviert ist, kann das Compose-Test-Framework jetzt die Uhr vorrücken und ausstehende Aufgaben verarbeiten, auch wenn blockierende Aufrufe im Hauptthread erfolgen.

Wann sollte die Synchronisierung des Hauptthreads verwendet werden?

Das Ausführen von Tests im Hintergrundthread ist zwar weiterhin der Standard für reine Compose-Tests, die Synchronisierung im Hauptthread bietet jedoch in einigen spezifischen Szenarien erhebliche Vorteile:

  • Komplexe View-Interoperabilität: Beim Testen von hybriden Benutzeroberflächen, die sowohl Compose- als auch Legacy-Android-Views enthalten, müssen Views oft im Hauptthread ausgeführt werden. Sie können jetzt nacheinander mit Ansichten interagieren und Assertions für Compose-Knoten ausführen, ohne ständig den Thread-Kontext wechseln zu müssen.
  • Synchrone Statusänderungen: Wenn Ihre Architektur auf strikt an den Hauptthread gebundenen State Holdern basiert, können Sie jetzt den Status ändern und sofort warten, bis sich die Compose-UI stabilisiert hat, 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 ohne Delegierung an Hintergrundthreads ausgeführt.

Beispiel

Da die Synchronisierung im Hauptthread früher streng 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 Hauptthread-Synchronisierung aktiviert ist, können die Assertions für Compose- und Ansichtshierarchien 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. das Laden von Daten oder das Messen oder Zeichnen von Android (d. h. Messen oder Zeichnen außerhalb von Compose), sollte ein allgemeineres Konzept wie waitUntil() verwendet werden:

composeTestRule.waitUntil(timeoutMs) { condition }

Sie können auch einen der waitUntil-Hilfsprogramme 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.