Konfigurowanie środowiska testowego za pomocą interfejsów API testowania w wersji 2

Już jest dostępna wersja 2 interfejsów API do testowania Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest itp.), które zapewniają lepszą kontrolę nad wykonywaniem współprogramów. Ta aktualizacja nie powiela całej powierzchni interfejsu API. Zaktualizowano tylko interfejsy API, które tworzą środowisko testowe.

Interfejsy API w wersji 1 zostały wycofane i zdecydowanie zalecamy przejście na nowe interfejsy API. Migracja pozwala sprawdzić, czy testy są zgodne ze standardowym zachowaniem współprogramów, i uniknąć problemów ze zgodnością w przyszłości. Listę wycofanych interfejsów API w wersji 1 znajdziesz w artykule o mapowaniu interfejsów API.

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

Konfigurowanie środowiska testowego

Interfejsy API Compose test w wersji 2 używają ComposeUiTestConfig do dostosowywania środowiska testowego. Interfejsy API, które tworzą funkcje konfiguracji testów, takie jak createComposeRule, runComposeUiTest i inne powiązane interfejsy API, akceptują ComposeUiTestConfig. Ten obiekt konfiguracji konsoliduje interfejsy API związane ze środowiskiem, takie jak effectContext, runTestContext i testTimeout, w jeden obiekt.

Model konfiguracji zarządza też inputMode. Interfejsy API Compose Test w wersji 2 domyślnie wymuszają InputMode.Touch na początku każdego testu, aby zapewnić determinizm i zapobiec wyciekowi stanu trybu wprowadzania między testami.

ComposeUiTestConfig jest częścią interfejsów API Compose Testing w wersji 2, które domyślnie używają StandardTestDispatcher. Jeśli Twoje testy korzystają z interfejsów API w wersji 1, przed wdrożeniem ComposeUiTestConfig zapoznaj się z artykułem Migracja do interfejsów API do testowania w wersji 2.

Migracja do ComposeUiTestConfig

W przypadku przeciążeń do tworzenia funkcji konfiguracji w testach kilka przeciążeń, które akceptują poszczególne parametry konfiguracji, takie jak effectContext, runTestContext lub testTimeout, zostało wycofanych. Zaktualizuj testy, aby używały ComposeUiTestConfig, jak w tym przykładzie:

Domyślny tryb wprowadzania

Testy mogą się nie powieść podczas migracji, jeśli opierają się na trybach wprowadzania danych innych niż dotykowe, które zostały skonfigurowane za pomocą interfejsów API instrumentacji przed rozpoczęciem testu. W funkcjach konfiguracji testów system domyślnie wymusza InputMode.Touch na początku każdego testu, aby zapewnić większy determinizm i zapobiec wyciekowi stanu, zastępując stan urządzenia i konfigurację przed testem.

Aby rozwiązać ten problem, określ wymagany tryb wprowadzania w ComposeUiTestConfig:

class FocusTest {
    @get:Rule
    val rule = createComposeRule(
        config = ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    )

    @Test
    fun testFocus() {}
}

Aby skonfigurować tryb wprowadzania dla poszczególnych elementów testowania, a nie dla całej klasy testowej, przekaż ComposeUiTestConfig do 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
    }
}

Informacje o innych problemach z migracją i sposobach ich rozwiązywania znajdziesz w artykule Najczęstsze błędy i sposoby ich rozwiązywania.

Przejście na interfejsy API do testowania w wersji 2

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

Możesz też poprosić Gemini o przeprowadzenie migracji do wersji 2 interfejsów API do testowania Compose za pomocą tego prompta:

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

Ten prompt będzie używać tego przewodnika do migracji na interfejsy API do 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 tej tabeli znajdziesz wycofane interfejsy API w wersji 1 i ich odpowiedniki w wersji 2:

Wycofane (v1)

Wymiana (v2)

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

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

Jedynym wyjątkiem, w którym działanie domyślne uległo zmianie, jest:

Domyślny dyspozytor testowy 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.

Najważniejsza zmiana: wpływ na wykonywanie współprogramów

Główna różnica między wersjami 1 i 2 interfejsów API polega na sposobie wysyłania współprogramów:

  • Interfejsy API w wersji 1 (UnconfinedTestDispatcher): po uruchomieniu współprogramu był on natychmiast wykonywany w bieżącym wątku, często kończąc się przed uruchomieniem następnego wiersza kodu testowego. W przeciwieństwie do zachowania w środowisku produkcyjnym to natychmiastowe wykonanie może nieumyślnie maskować rzeczywiste problemy z czasem lub sytuacje wyścigu, które wystąpiłyby w aktywnej aplikacji.
  • Interfejsy API w wersji 2 (StandardTestDispatcher): po uruchomieniu współprogramu jest on umieszczany w kolejce i nie jest wykonywany, dopóki test nie przesunie zegara wirtualnego. Standardowe interfejsy API do testowania Compose (takie jak waitForIdle()) obsługują już tę synchronizację, więc większość testów opartych na tych standardowych interfejsach API powinna nadal działać bez zmian.

Najczęstsze błędy i rozwiązanie problemu

Jeśli po przejściu na wersję 2 testy się nie powiodą, prawdopodobnie wystąpią te problemy:

  • Niepowodzenie: uruchamiasz zadanie (np. ViewModel wczytuje dane), ale asercja natychmiast kończy się niepowodzeniem, ponieważ dane są nadal w stanie „Wczytywanie”.
  • Przyczyna: w przypadku interfejsów API w wersji 2 współprogramy są umieszczane w kolejce, a nie wykonywane natychmiast. Zadanie zostało dodane do kolejki, ale nie zostało uruchomione przed sprawdzeniem wyniku.
  • Rozwiązanie: wyraźnie przesuń czas do przodu. Musisz wyraźnie poinformować dyspozytora w wersji 2, kiedy ma wykonać zadanie.

Poprzednia metoda

W wersji 1 zadanie uruchamiało się i natychmiast kończyło. W wersji 2 ten 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 asercją.

Opcja 1. Użycie waitForIdle przesuwa zegar do momentu, gdy interfejs użytkownika jest bezczynny, co potwierdza, że współprogram został uruchomiony.

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 UI, gdy interfejs przejdzie w stan bezczynności.

viewModel.loadData()

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

Ręczna synchronizacja

W scenariuszach obejmujących ręczną synchronizację, np. gdy automatyczne przechodzenie jest wyłączone, uruchomienie współprogramu nie powoduje natychmiastowego wykonania, ponieważ zegar testowy jest wstrzymany. Aby wykonać współprogramy w kolejce bez przesunięcia zegara wirtualnego, użyj interfejsu API runCurrent(). Spowoduje to uruchomienie zadań zaplanowanych na bieżący czas wirtualny.

composeTestRule.mainClock.scheduler.runCurrent()

W przeciwieństwie do funkcji waitForIdle(), która przesuwa zegar testowy 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.

Podstawowy harmonogram testów używany w środowisku testowym jest widoczny. Tego harmonogramu można używać w połączeniu z interfejsem API runTest w języku Kotlin do synchronizowania zegara testowego.

Migracja do runComposeUiTest

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

Poprzednia metoda

Użycie createComposeRule w połączeniu z runTest powoduje utworzenie 2 osobnych zegarów: jednego dla Compose i jednego dla zakresu testowej współprogramu. 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 API runComposeUiTest automatycznie wykonuje blok testowy w 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()
    }
}