Die v2-Versionen der Compose-Test-APIs (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest usw.) sind jetzt verfügbar, um die Kontrolle über die Coroutine-Ausführung zu verbessern. Bei diesem Update wird nicht die gesamte API-Oberfläche dupliziert. Es wurden nur die APIs aktualisiert, die die Testumgebung einrichten.
Die v1-APIs wurden eingestellt. Es wird dringend empfohlen, zu den neuen APIs zu migrieren. Durch die Migration wird sichergestellt, dass Ihre Tests dem Standardverhalten von Koroutinen entsprechen, und zukünftige Kompatibilitätsprobleme werden vermieden. Eine Liste der verworfenen v1-APIs finden Sie unter API-Zuordnungen.
Während die v1 APIs auf der UnconfinedTestDispatcher basierten, verwenden die v2 APIs standardmäßig die StandardTestDispatcher für die laufende Komposition. Durch diese Änderung wird das Verhalten von Compose-Tests an die standardmäßigen runTest APIs angepasst und die Ausführungsreihenfolge von Koroutinen kann explizit gesteuert werden.
Testumgebung konfigurieren
Compose Test v2 APIs verwenden ComposeUiTestConfig, um die Testumgebung anzupassen. APIs, die Einrichtungsfunktionen für Tests erstellen, z. B. createComposeRule, runComposeUiTest und andere zugehörige APIs, akzeptieren ComposeUiTestConfig. Dieses Konfigurationsobjekt fasst umgebungsbezogene APIs wie effectContext, runTestContext und testTimeout in einem einzigen Objekt zusammen.
Das Konfigurationsmodell verwaltet auch inputMode. Compose Test v2 APIs erzwingen standardmäßig zu Beginn jedes Tests InputMode.Touch, um Determinismus zu gewährleisten und zu verhindern, dass der Status des Eingabemodus zwischen Tests verloren geht.
ComposeUiTestConfig ist Teil der Compose-Test-APIs der Version 2, die standardmäßig StandardTestDispatcher verwenden. Wenn Ihre Tests v1 APIs verwenden, lesen Sie den Abschnitt Zu v2-Test-APIs migrieren, bevor Sie ComposeUiTestConfig verwenden.
Migrieren Sie zu ComposeUiTestConfig
In den Überladungen zum Erstellen von Einrichtungsfunktionen in Tests sind mehrere Überladungen, die einzelne Konfigurationsparameter wie effectContext, runTestContext oder testTimeout akzeptieren, veraltet. Aktualisieren Sie Ihre Tests, um stattdessen ComposeUiTestConfig zu verwenden, wie im folgenden Beispiel gezeigt:
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
Standardeingabemodus
Tests können während der Migration fehlschlagen, wenn sie auf Eingabemodi ohne Eingabe per Berührung angewiesen sind, die vor dem Teststart über Instrumentierungs-APIs konfiguriert wurden. In den Einrichtungsfunktionen für Tests erzwingt das System standardmäßig zu Beginn jedes Tests InputMode.Touch, um mehr Determinismus zu erreichen und Statuslecks zu verhindern. Dabei werden der Umgebungsgerätestatus und die Einrichtung vor dem Test überschrieben.
Geben Sie den erforderlichen Eingabemodus in ComposeUiTestConfig an, um dieses Problem zu beheben:
class FocusTest { @get:Rule val rule = createComposeRule( config = ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) @Test fun testFocus() {} }
Wenn Sie den Eingabemodus für einzelne Testfälle anstelle der gesamten Testklasse konfigurieren möchten, übergeben Sie ComposeUiTestConfig an runComposeUiTest:
class FocusTest { @Test fun testTouchMode() = runComposeUiTest { // Runs with the default InputMode.Touch } @Test fun testKeyboardMode() = runComposeUiTest( ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) { // Runs with InputMode.Keyboard } }
Weitere Informationen zu Migrationsproblemen und deren Behebung finden Sie unter Häufige Fehler und ihre Behebung.
Zu v2-Test-APIs migrieren
Beim Upgrade auf v2-APIs können Sie im Allgemeinen Suchen und Ersetzen verwenden, um die Paketimporte zu aktualisieren und die neuen Dispatcher-Änderungen zu übernehmen.
Alternativ können Sie Gemini mit dem folgenden Prompt auffordern, eine Migration zu Version 2 der Compose-Test-APIs durchzuführen:
KI-Prompt
Von v1-Test-APIs zu v2-Test-APIs migrieren
In diesem Prompt wird diese Anleitung verwendet, um zu den Test-APIs der Version 2 zu migrieren.
Migrate to Compose testing v2 APIs using the official
migration guide.In der folgenden Tabelle finden Sie die Zuordnung der eingestellten v1-APIs zu den entsprechenden v2-APIs:
Eingestellt (v1) |
Ersatz (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Abwärtskompatibilität und Ausnahmen
Die vorhandenen v1-APIs sind jetzt verworfen, 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:
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 oder der abgeleiteten Klasse AndroidComposeUiTestEnvironment erstellen und diesen Konstruktor aufrufen.
Wichtige Änderung: Auswirkungen auf die Ausführung von Koroutinen
Der Hauptunterschied zwischen Version 1 und Version 2 der APIs besteht darin, wie Koroutinen weitergeleitet werden:
- v1 APIs (
UnconfinedTestDispatcher): Wenn eine Koroutine gestartet wurde, wurde sie sofort im aktuellen Thread ausgeführt und oft beendet, bevor die nächste Zeile des Testcodes ausgeführt wurde. Im Gegensatz zum Verhalten in der Produktion könnte diese sofortige Ausführung unbeabsichtigt echte Timing-Probleme oder Race-Bedingungen maskieren, die in einer Live-Anwendung auftreten würden. - v2 APIs (
StandardTestDispatcher): Wenn eine Koroutine gestartet wird, wird sie in die Warteschlange gestellt und erst ausgeführt, wenn der Test die virtuelle Uhr explizit vorstellt. Standard-Compose-Test-APIs (wiewaitForIdle()) ü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 v2 fehlschlagen, weisen sie wahrscheinlich das folgende Muster auf:
- Fehler: Sie starten eine Aufgabe (z. B. lädt ein ViewModel Daten), aber Ihre Assertion schlägt sofort fehl, weil sich die Daten noch im Status „Wird geladen“ befinden.
- Ursache: Bei den v2 APIs werden Koroutinen 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.
- Lösung: Zeit explizit vorrücken. Sie müssen dem v2-Dispatcher explizit mitteilen, wann die Arbeit ausgeführt werden soll.
Bisheriger Ansatz
In v1 wurde die Aufgabe sofort gestartet und abgeschlossen. In v2 schlägt der folgende Code fehl, da 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 Aufgaben in der Warteschlange vor der Assertion auszuführen.
Option 1: Mit waitForIdle wird die Uhr so lange vorgestellt, bis die Benutzeroberfläche inaktiv ist. Dadurch wird überprüft, ob die Koroutine ausgeführt wurde.
viewModel.loadData()
// Explicitly run all queued tasks
composeTestRule.waitForIdle()
assertEquals(Success, viewModel.state.value)
Option 2: Mit runOnIdle 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
In Szenarien mit manueller Synchronisierung, z. B. wenn der automatische Übergang deaktiviert ist, führt das Starten einer Koroutine nicht zur sofortigen Ausführung, da die Testuhr pausiert ist. Wenn Sie Koroutinen 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(), das die Testuhr so lange vorstellt, bis sich die Benutzeroberfläche stabilisiert, führt runCurrent() ausstehende Aufgaben aus, während die aktuelle virtuelle Zeit beibehalten wird. So können Zwischenzustände überprüft werden, die sonst übersprungen würden, wenn die Uhr auf einen inaktiven Zustand vorgerückt würde.
Der zugrunde liegende Testplaner, der in der Testumgebung verwendet wird, wird angezeigt. Dieser Planer kann in Verbindung mit der Kotlin-runTest-API verwendet werden, um die Testuhr zu synchronisieren.
Migrieren Sie zu runComposeUiTest
Wenn Sie Compose-Test-APIs zusammen mit der Kotlin-API runTest verwenden, sollten Sie unbedingt zu runComposeUiTest wechseln.
Bisheriger Ansatz
Wenn Sie createComposeRule in Verbindung mit runTest verwenden, werden zwei separate Uhren erstellt: eine für Compose und eine für den Test-Koroutinenbereich. Diese Konfiguration kann dazu führen, dass Sie den Testplaner manuell synchronisieren müssen.
@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 den Testblock automatisch im eigenen runTest-Bereich aus. Die Testuhr wird mit der Compose-Umgebung synchronisiert, sodass Sie den Planer 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() } }