Przejście na interfejsy API testowania w wersji 2

Wersje 2 interfejsów API do testowania Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest itp.) są teraz dostępne, aby zwiększyć kontrolę nad wykonywaniem korutyn. Ta aktualizacja nie obejmuje wszystkich interfejsów API, tylko te, które tworzą środowisko testowe.

Interfejsy API w wersji 1 zostały wycofane i zdecydowanie zalecamy przejście na nowe interfejsy API. Migracja potwierdza, że testy są zgodne ze standardowym działaniem współprogramów, i pozwala uniknąć przyszłych problemów ze zgodnością. Listę wycofanych interfejsów API w wersji 1 znajdziesz w mapowaniu interfejsów API.

Te zmiany są uwzględnione w androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+androidx.compose.ui:ui-test:1.11.0-alpha03+.

Interfejsy API w wersji 1 korzystały z UnconfinedTestDispatcher, a interfejsy API w wersji 2 domyślnie używają StandardTestDispatcher do uruchamiania kompozycji. Ta zmiana dostosowuje działanie testów w Compose do standardowych interfejsów API runTest i zapewnia wyraźną kontrolę nad kolejnością wykonywania korutyn.

Mapowania interfejsu API

Podczas uaktualniania do interfejsów API w wersji 2 możesz zwykle użyć funkcji Znajdź i zamień, aby zaktualizować importy pakietów i wprowadzić nowe zmiany w dispatcherze.

Możesz też poprosić Gemini o przeprowadzenie migracji do wersji 2 interfejsów API testowania Compose, używając tego prompta:

Przechodzenie z interfejsów API do testowania w wersji 1 na interfejsy API do testowania w wersji 2

Ten prompt użyje tego przewodnika, aby przeprowadzić migrację do interfejsów API testowania w wersji 2.

Migrate to Compose testing v2 APIs using the official
migration guide.

Korzystanie z promptów AI

Prompty AI są przeznaczone do użytku w Gemini w Android Studio.

Więcej informacji o Gemini w Studio znajdziesz tutaj: https://developer.android.com/studio/gemini/overview

W tabeli poniżej znajdziesz mapowanie wycofanych interfejsów API w wersji 1 na ich odpowiedniki w wersji 2:

Wycofana (v1)

Wymiana (wersja 2)

androidx.compose.ui.test.junit4.createComposeRule

androidx.compose.ui.test.junit4.v2.createComposeRule

androidx.compose.ui.test.junit4.createAndroidComposeRule

androidx.compose.ui.test.junit4.v2.createAndroidComposeRule

androidx.compose.ui.test.junit4.createEmptyComposeRule

androidx.compose.ui.test.junit4.v2.createEmptyComposeRule

androidx.compose.ui.test.junit4.AndroidComposeTestRule

androidx.compose.ui.test.junit4.v2.AndroidComposeTestRule

androidx.compose.ui.test.runComposeUiTest

androidx.compose.ui.test.v2.runComposeUiTest

androidx.compose.ui.test.runAndroidComposeUiTest

androidx.compose.ui.test.v2.runAndroidComposeUiTest

androidx.compose.ui.test.runEmptyComposeUiTest

androidx.compose.ui.test.v2.runEmptyComposeUiTest

androidx.compose.ui.test.AndroidComposeUiTestEnvironment

androidx.compose.ui.test.v2.AndroidComposeUiTestEnvironment

Zgodność wsteczna i wyjątki

Obecne interfejsy API w wersji 1 są już wycofane, ale nadal używają UnconfinedTestDispatcher, aby zachować dotychczasowe działanie i zapobiec zmianom powodującym błędy.

Jedynym wyjątkiem, w przypadku którego zmieniliśmy działanie domyślne, jest:

Domyślny dyspozytor testów używany do uruchamiania kompozycji w klasie AndroidComposeUiTestEnvironment został zmieniony z UnconfinedTestDispatcher na StandardTestDispatcher. Dotyczy to przypadków, w których tworzysz instancję za pomocą konstruktora lub podklasy AndroidComposeUiTestEnvironment i wywołujesz ten konstruktor.

Kluczowa zmiana: wpływ na wykonywanie współprogramów

Główna różnica między interfejsami API w wersji 1 i 2 polega na sposobie wysyłania korutyn:

  • Interfejsy API w wersji 1 (UnconfinedTestDispatcher): po uruchomieniu korutyny była ona natychmiast wykonywana w bieżącym wątku i często kończyła się przed uruchomieniem następnego wiersza kodu testu. W odróżnieniu od zachowania w wersji produkcyjnej to natychmiastowe wykonanie może nieumyślnie maskować rzeczywiste problemy z czasem lub warunki wyścigu, które wystąpiłyby w aktywnej aplikacji.
  • Interfejsy API w wersji 2 (StandardTestDispatcher): po uruchomieniu korutyny jest ona umieszczana w kolejce i nie jest wykonywana, dopóki test nie przesunie wirtualnego zegara. Standardowe interfejsy API do testowania Compose (np. waitForIdle()) już obsługują tę synchronizację, więc większość testów korzystających z tych standardowych interfejsów API powinna nadal działać bez zmian.

Najczęstsze błędy i sposoby ich rozwiązywania

Jeśli po uaktualnieniu do wersji 2 testy się nie powiodą, prawdopodobnie będą miały następujący wzorzec:

  • Niepowodzenie: uruchamiasz zadanie (np. ViewModel wczytuje dane), ale Twoje stwierdzenie natychmiast kończy się niepowodzeniem, ponieważ dane są nadal w stanie „Wczytywanie”.
  • Przyczyna: w przypadku interfejsów API w wersji 2 korutyny są umieszczane w kolejce zamiast być wykonywane natychmiast. Zadanie zostało umieszczone w kolejce, ale nigdy nie zostało uruchomione przed sprawdzeniem wyniku.
  • Napraw: wyraźnie przesuń czas do przodu. Musisz wyraźnie poinformować dyspozytora v2, kiedy ma wykonać pracę.

Poprzednie podejście

W wersji 1 zadanie uruchamiało się i kończyło natychmiast. W wersji 2 poniższy kod nie działa, ponieważ funkcja loadData() nie została jeszcze uruchomiona.

// 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)

Użyj waitForIdle lub runOnIdle, aby wykonać zadania w kolejce przed potwierdzeniem.

Opcja 1: użycie waitForIdle powoduje przesunięcie zegara do momentu, w którym interfejs użytkownika jest bezczynny, co potwierdza, że korutyna została uruchomiona.

viewModel.loadData()

// Explicitly run all queued tasks
composeTestRule.waitForIdle()

assertEquals(Success, viewModel.state.value)

Opcja 2: użycie runOnIdle powoduje wykonanie bloku kodu w wątku interfejsu po tym, jak interfejs przejdzie w stan bezczynności.

viewModel.loadData()

// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
    assertEquals(Success, viewModel.state.value)
}

Synchronizacja ręczna

W scenariuszach obejmujących ręczną synchronizację, np. gdy automatyczne przechodzenie do następnego kroku jest wyłączone, uruchomienie korutyny nie powoduje natychmiastowego wykonania, ponieważ zegar testowy jest wstrzymany. Aby wykonać w kolejce współprogramy bez przesuwania wirtualnego zegara, użyj interfejsu API runCurrent(). Uruchamia zadania zaplanowane na bieżący czas wirtualny.

composeTestRule.mainClock.scheduler.runCurrent()

W przeciwieństwie do funkcji waitForIdle(), która przesuwa zegar testu do momentu ustabilizowania się interfejsu, funkcja runCurrent() wykonuje oczekujące zadania, zachowując bieżący czas wirtualny. Takie działanie umożliwia weryfikację stanów pośrednich, które w przeciwnym razie zostałyby pominięte, gdyby zegar został przesunięty do stanu bezczynności.

Udostępniany jest podstawowy harmonogram testów używany w środowisku testowym. Ten harmonogram może być używany w połączeniu z interfejsem runTest API w języku Kotlin, aby synchronizować zegar testowy.

Migracja do runComposeUiTest

Jeśli używasz interfejsów API testów Compose razem z interfejsem API Kotlin runTest, zdecydowanie zalecamy przejście na runComposeUiTest.

Poprzednie podejście

Użycie createComposeRule w połączeniu z runTest tworzy 2 osobne zegary: jeden dla funkcji Compose i jeden dla zakresu testowej korutyny. Ta konfiguracja może wymusić ręczną synchronizację harmonogramu testów.

@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()
    }
}

Interfejs runComposeUiTest API automatycznie wykonuje blok testowy w swoim zakresie.runTest Zegar testowy jest zsynchronizowany ze środowiskiem Compose, więc nie musisz już ręcznie zarządzać harmonogramem.

    @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()
    }
}