Tworzenie interfejsów API testów 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 współprogramów. 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ąć problemów ze zgodnością w przyszłości. Listę wycofanych interfejsów API w wersji 1 znajdziesz w mapowaniach interfejsów API.

Zmiany te są uwzględnione w dokumentach 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 współprogramów.

Konfigurowanie środowiska testowego

Interfejsy Compose Test API w wersji 2 używają parametru 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 łączy interfejsy API związane z ochroną środowiska, takie jak effectContext, runTestContexttestTimeout, w jeden obiekt.

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

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

Migracja do ComposeUiTestConfig

W przypadku przeciążeń funkcji 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 zamiast tego używać parametru ComposeUiTestConfig, jak pokazano w tym przykładzie:

Domyślny tryb wprowadzania

Testy mogą się nie powieść podczas migracji, jeśli przed rozpoczęciem testu korzystają z trybów wprowadzania danych innych niż dotykowe skonfigurowanych za pomocą interfejsów API instrumentacji. W funkcjach konfiguracji testów system domyślnie wymusza InputMode.Touch na początku każdego testu, aby zapewnić większą deterministyczność 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 przypadków testowych zamiast 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
    }
}

Więcej informacji 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 testowania w wersji 2

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 wycofane interfejsy API w wersji 1 i ich odpowiedniki w wersji 2:

Wycofana (v1)

Zamiennik (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 którym domyślne działanie uległo zmianie, 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 korutyn

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 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 odróżnieniu od działania w środowisku produkcyjnym 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 współprogramu jest ona umieszczana w kolejce i nie jest wykonywana, dopóki test nie przesunie wirtualnego zegara. Standardowe interfejsy API testów 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.

Typowe 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:

  • Błąd: uruchamiasz zadanie (np. ViewModel wczytuje dane), ale asercja natychmiast się nie powodzi, ponieważ dane są nadal w stanie „Wczytywanie”.
  • Przyczyna: w przypadku interfejsów API w wersji 2 korutyny są umieszczane w kolejce zamiast wykonywać się 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 było uruchamiane i kończone natychmiast. 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 potwierdzeniem.

Opcja 1: użycie waitForIdle powoduje przesunięcie zegara do momentu, w którym 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 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 współprogramu nie powoduje natychmiastowego wykonania, ponieważ zegar testowy jest wstrzymany. Aby wykonać w kolejce współprogramy bez przesuwania zegara wirtualnego, 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 bazowy harmonogram testów używany w środowisku testowym. Ten harmonogram może być używany w połączeniu z interfejsem Kotlin runTest API do synchronizowania zegara testowego.

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 oddzielne zegary: jeden dla funkcji Compose i jeden dla zakresu testowego 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 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()
    }
}