Die Version 2 der Compose-Test-APIs (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest usw.) ist jetzt verfügbar, um die Steuerung der Coroutine-Ausführung zu verbessern. Bei diesem Update wird nicht die gesamte API-Oberfläche dupliziert, sondern nur die APIs, die die Testumgebung einrichten.
Die v1-APIs sind veraltet. Wir empfehlen dringend, zu den neuen APIs zu migrieren. Durch die Migration wird sichergestellt, dass Ihre Tests dem Standardverhalten von Coroutinen entsprechen, und zukünftige Kompatibilitätsprobleme werden vermieden. Eine Liste der verworfenen v1-APIs finden Sie unter API-Zuweisungen.
Diese Änderungen sind in androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+ und androidx.compose.ui:ui-test:1.11.0-alpha03+ enthalten.
Während die v1-APIs auf UnconfinedTestDispatcher angewiesen waren, verwenden die v2-APIs standardmäßig StandardTestDispatcher für die laufende Komposition. Durch diese Änderung wird das Compose-Testverhalten an die Standard-runTest-APIs angepasst und die Ausführungsreihenfolge von Coroutinen kann explizit gesteuert werden.
API-Zuordnungen
Beim Upgrade auf die APIs der Version 2 können Sie in der Regel Suchen + Ersetzen verwenden, um die Paketimporte zu aktualisieren und die neuen Änderungen am Dispatcher zu übernehmen.
Alternativ können Sie Gemini mit dem folgenden Prompt auffordern, eine Migration zur Version 2 der Compose-Test-APIs durchzuführen:
KI-Prompt
Von v1-Test-APIs zu v2-Test-APIs migrieren
Dieser Prompt hilft Ihnen bei der Migration zu den V2-Test-APIs.
Migrate to Compose testing v2 APIs using the official
migration guide.In der folgenden Tabelle sehen Sie, wie die eingestellten V1-APIs den V2-APIs zugeordnet werden:
Verworfen (Version 1) |
Replacement (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Abwärtskompatibilität und Ausnahmen
Die vorhandenen v1-APIs sind jetzt veraltet, verwenden aber weiterhin UnconfinedTestDispatcher, um das vorhandene Verhalten beizubehalten und funktionsgefährdende Änderungen zu verhindern.
Die einzige Ausnahme, bei der sich das Standardverhalten geändert hat, ist folgende:
Der Standard-Test-Dispatcher, der zum Ausführen der Komposition in der Klasse AndroidComposeUiTestEnvironment verwendet wird, wurde von UnconfinedTestDispatcher zu StandardTestDispatcher geändert. Dies betrifft Fälle, in denen Sie eine Instanz mit dem Konstruktor erstellen oder eine Unterklasse von AndroidComposeUiTestEnvironment erstellen und diesen Konstruktor aufrufen.
Wichtige Änderung: Auswirkungen auf die Ausführung von Coroutinen
Der Hauptunterschied zwischen Version 1 und Version 2 der APIs besteht darin, wie Coroutinen verteilt werden:
- v1-APIs (
UnconfinedTestDispatcher): Wenn eine Coroutine gestartet wurde, wurde sie sofort im aktuellen Thread ausgeführt und war oft abgeschlossen, bevor die nächste Zeile des Testcodes ausgeführt wurde. Im Gegensatz zum Produktionsverhalten kann diese sofortige Ausführung versehentlich echte Timing-Probleme oder Race Conditions maskieren, die in einer aktiven Anwendung auftreten würden. - v2-APIs (
StandardTestDispatcher): Wenn eine Coroutine gestartet wird, wird sie in die Warteschlange gestellt und erst ausgeführt, wenn die virtuelle Uhr im Test explizit weitergestellt wird. Standard-Compose-Test-APIs (z. B.waitForIdle()) übernehmen diese Synchronisierung bereits. Die meisten Tests, die auf diesen Standard-APIs basieren, sollten daher ohne Änderungen weiterhin funktionieren.
Häufige Fehler und ihre Behebung
Wenn Ihre Tests nach dem Upgrade auf Version 2 fehlschlagen, weisen sie wahrscheinlich das folgende Muster auf:
- Fehler: Sie starten eine Aufgabe (z. B. lädt ein ViewModel Daten), aber die Assertion schlägt sofort fehl, weil sich die Daten noch im Status „Wird geladen“ befinden.
- Ursache: Bei den APIs der Version 2 werden Coroutinen in die Warteschlange gestellt, anstatt sofort ausgeführt zu werden. Die Aufgabe wurde in die Warteschlange gestellt, aber nie ausgeführt, bevor das Ergebnis geprüft wurde.
- Korrektur: Die Zeit wird explizit vorverlegt. Sie müssen dem v2-Dispatcher explizit mitteilen, wann er Aufgaben ausführen soll.
Bisheriger Ansatz
In Version 1 wurde die Aufgabe sofort gestartet und beendet. In Version 2 schlägt der folgende Code fehl, weil loadData() noch nicht ausgeführt wurde.
// In v1, this launched and finished immediately.
viewModel.loadData()
// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)
Empfohlene Vorgehensweise
Verwenden Sie waitForIdle oder runOnIdle, um in die Warteschlange gestellte Aufgaben vor dem Assert auszuführen.
Option 1: Wenn Sie waitForIdle verwenden, wird die Uhr bis zum Leerlauf der Benutzeroberfläche vorgespult. So wird überprüft, ob die Coroutine ausgeführt wurde.
viewModel.loadData()
// Explicitly run all queued tasks
composeTestRule.waitForIdle()
assertEquals(Success, viewModel.state.value)
Option 2: Wenn Sie runOnIdle verwenden, wird der Codeblock im UI-Thread ausgeführt, nachdem die UI inaktiv geworden ist.
viewModel.loadData()
// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
assertEquals(Success, viewModel.state.value)
}
Manuelle Synchronisierung
Bei Szenarien mit manueller Synchronisierung, z. B. wenn das automatische Vorrücken deaktiviert ist, führt das Starten einer Coroutine nicht zur sofortigen Ausführung, da die Testuhr angehalten wird. Wenn Sie Coroutinen in der Warteschlange ausführen möchten, ohne die virtuelle Uhr vorzustellen, verwenden Sie die runCurrent() API. Dadurch werden Aufgaben ausgeführt, die für die aktuelle virtuelle Zeit geplant sind.
composeTestRule.mainClock.scheduler.runCurrent()
Im Gegensatz zu waitForIdle(), bei der die Testuhr so lange voranschreitet, bis die Benutzeroberfläche stabil ist, werden bei runCurrent() ausstehende Aufgaben ausgeführt, während die aktuelle virtuelle Zeit beibehalten wird. Dieses Verhalten ermöglicht die Überprüfung von Zwischenzuständen, die andernfalls übersprungen würden, wenn die Zeit auf einen Leerlaufzustand vorgerückt würde.
Der zugrunde liegende Test-Scheduler, der in der Testumgebung verwendet wird, wird verfügbar gemacht. Dieser Scheduler kann in Verbindung mit der Kotlin-API runTest verwendet werden, um die Testuhr zu synchronisieren.
Zu runComposeUiTest migrieren
Wenn Sie Compose-Test-APIs zusammen mit der Kotlin-runTest-API verwenden, wird dringend empfohlen, zu runComposeUiTest zu wechseln.
Bisheriger Ansatz
Wenn Sie createComposeRule in Verbindung mit runTest verwenden, werden zwei separate Zeitgeber erstellt: einer für Compose und einer für den Test-Coroutine-Scope. Bei dieser Konfiguration müssen Sie den Test-Scheduler möglicherweise manuell synchronisieren.
@get:Rule val composeTestRule = createComposeRule() @Test fun testWithCoroutines() { composeTestRule.setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } // NOT RECOMMENDED // Fails: runTest creates a new, separate scheduler. // Advancing time here does NOT advance the compose clock. // To fix this without migrating, you would need to share the scheduler // by passing 'composeTestRule.mainClock.scheduler' to runTest. runTest { composeTestRule.onNodeWithText("Loading...").assertIsDisplayed() advanceTimeBy(1000) composeTestRule.onNodeWithText("Done!").assertIsDisplayed() } }
Empfohlene Vorgehensweise
Die runComposeUiTest API führt Ihren Testblock automatisch in ihrem eigenen runTest-Bereich aus. Die Testuhr wird mit der Compose-Umgebung synchronisiert, sodass Sie den Scheduler nicht mehr manuell verwalten müssen.
@Test fun testWithCoroutines() = runComposeUiTest { setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } onNodeWithText("Loading...").assertIsDisplayed() mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */) onNodeWithText("Done!").assertIsDisplayed() } }