API Compose test v2

Le versioni 2 delle API di test Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest e così via) sono ora disponibili per migliorare il controllo sull'esecuzione delle coroutine. Questo aggiornamento non duplica l'intera superficie dell'API, ma solo le API che stabiliscono l'ambiente di test.

Le API v1 sono obsolete ed è vivamente consigliato eseguire la migrazione alle nuove API. La migrazione verifica che i test siano in linea con il comportamento standard delle coroutine ed evita futuri problemi di compatibilità. Per un elenco delle API v1 deprecate, consulta Mappature API.

Queste modifiche sono incluse in androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+ e androidx.compose.ui:ui-test:1.11.0-alpha03+.

Mentre le API v1 si basavano su UnconfinedTestDispatcher, le API v2 utilizzano StandardTestDispatcher per impostazione predefinita per la composizione in esecuzione. Questa modifica allinea il comportamento dei test di Compose alle API runTest standard e fornisce un controllo esplicito sull'ordine di esecuzione delle coroutine.

Configurare l'ambiente di test

Le API Compose test v2 utilizzano ComposeUiTestConfig per personalizzare l'ambiente di test. Le API che creano funzioni di configurazione per i test, come createComposeRule, runComposeUiTest e altre API correlate, accettano ComposeUiTestConfig. Questo oggetto di configurazione consolida le API correlate all'ambiente, come effectContext, runTestContext e testTimeout in un unico oggetto.

Il modello di configurazione gestisce anche inputMode. Le API Compose test v2 applicano InputMode.Touch per impostazione predefinita all'inizio di ogni test per garantire il determinismo e impedire che lo stato della modalità di input venga divulgato tra i test.

ComposeUiTestConfig fa parte delle API Compose Test v2, che utilizzano StandardTestDispatcher per impostazione predefinita. Se i tuoi test utilizzano le API v1, consulta la sezione Eseguire la migrazione alle API di test v2 prima di adottare ComposeUiTestConfig.

Migrazione a ComposeUiTestConfig

All'interno degli overload per la creazione di funzioni di configurazione nei test, diversi overload che accettano singoli parametri di configurazione, come effectContext, runTestContext o testTimeout, sono ritirati. Aggiorna i test in modo che utilizzino ComposeUiTestConfig, come mostrato nell'esempio seguente:

Modalità di input predefinita

I test possono non riuscire durante la migrazione se si basano su modalità di input non touch configurate tramite le API di strumentazione prima dell'inizio del test. All'interno delle funzioni di configurazione per i test, il sistema applica InputMode.Touch per impostazione predefinita all'inizio di ogni test per un maggiore determinismo e per impedire la perdita di stato, ignorando lo stato del dispositivo ambientale e la configurazione pre-test.

Per risolvere il problema, specifica la modalità di input richiesta in ComposeUiTestConfig:

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

    @Test
    fun testFocus() {}
}

Per configurare la modalità di input per singoli scenari di test anziché per l'intera classe di test, passa ComposeUiTestConfig a 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
    }
}

Per altri problemi di migrazione e relative soluzioni, vedi Errori comuni e come correggerli.

Esegui la migrazione alle API di test v2

Quando esegui l'upgrade alle API v2, in genere puoi utilizzare Trova e sostituisci per aggiornare le importazioni di pacchetti e adottare le nuove modifiche al dispatcher.

In alternativa, chiedi a Gemini di eseguire una migrazione alla versione 2 delle API Compose Testing con il seguente prompt:

Eseguire la migrazione dalle API di test v1 alle API di test v2

Questo prompt utilizzerà questa guida per eseguire la migrazione alle API di test v2.

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

Utilizzare i prompt AI

I prompt AI sono pensati per essere utilizzati in Gemini in Android Studio.

Scopri di più su Gemini in Studio qui: https://developer.android.com/studio/gemini/overview

Utilizza la tabella seguente per mappare le API v1 ritirate alle relative sostituzioni v2:

Deprecato (v1)

Replacement (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

Compatibilità con le versioni precedenti ed eccezioni

Le API v1 esistenti sono ora obsolete, ma continuano a utilizzare UnconfinedTestDispatcher per mantenere il comportamento esistente ed evitare modifiche che potrebbero causare problemi.

Di seguito è riportata l'unica eccezione in cui il comportamento predefinito è cambiato:

Il dispatcher di test predefinito utilizzato per l'esecuzione della composizione nella classe AndroidComposeUiTestEnvironment è passato da UnconfinedTestDispatcher a StandardTestDispatcher. Ciò influisce sui casi in cui crei un'istanza utilizzando il costruttore o la sottoclasse AndroidComposeUiTestEnvironment e chiami il costruttore.

Modifica principale: impatto sull'esecuzione delle coroutine

La differenza principale tra le API v1 e v2 è il modo in cui vengono inviate le coroutine:

  • API v1 (UnconfinedTestDispatcher): quando veniva avviata una coroutine, veniva eseguita immediatamente sul thread corrente, spesso terminando prima dell'esecuzione della riga successiva di codice di test. A differenza del comportamento di produzione, questa esecuzione immediata potrebbe mascherare inavvertitamente problemi di sincronizzazione reali o race condition che si verificherebbero in un'applicazione live.
  • API v2 (StandardTestDispatcher): quando viene avviata una coroutine, viene messa in coda e non viene eseguita finché il test non fa avanzare esplicitamente l'orologio virtuale. Le API di test di composizione standard (ad esempio waitForIdle()) gestiscono già questa sincronizzazione, quindi la maggior parte dei test che si basano su queste API standard continueranno a funzionare senza modifiche.

Errori comuni e come correggerli

Se i test non superano l'upgrade alla versione 2, è probabile che mostrino il seguente pattern:

  • Errore: avvii un'attività (ad esempio, un ViewModel carica i dati), ma l'asserzione non va a buon fine immediatamente perché i dati sono ancora in stato "Caricamento".
  • Causa: con le API v2, le coroutine vengono messe in coda anziché eseguite immediatamente. L'attività è stata messa in coda, ma non è mai stata eseguita prima che il risultato venisse controllato.
  • Correzione: avanza esplicitamente l'ora. Devi indicare esplicitamente al dispatcher v2 quando eseguire il lavoro.

Approccio precedente

Nella versione 1, l'attività veniva avviata e completata immediatamente. Nella versione 2, il seguente codice non viene eseguito perché loadData() non è ancora stato eseguito.

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

Utilizza waitForIdle o runOnIdle per eseguire le attività in coda prima dell'asserzione.

Opzione 1: l'utilizzo di waitForIdle fa avanzare l'orologio fino a quando la UI non è inattiva, verificando che la coroutine sia stata eseguita.

viewModel.loadData()

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

assertEquals(Success, viewModel.state.value)

Opzione 2: l'utilizzo di runOnIdle esegue il blocco di codice sul thread dell'interfaccia utente dopo che la UI è diventata inattiva.

viewModel.loadData()

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

Sincronizzazione manuale

Negli scenari che prevedono la sincronizzazione manuale, ad esempio quando l'avanzamento automatico è disattivato, l'avvio di una coroutine non comporta l'esecuzione immediata perché l'orologio di test è in pausa. Per eseguire le coroutine nella coda senza far avanzare l'orologio virtuale, utilizza l'API runCurrent(). Esegue le attività pianificate per l'ora virtuale corrente.

composeTestRule.mainClock.scheduler.runCurrent()

A differenza di waitForIdle(), che fa avanzare l'orologio di test fino a quando la UI si stabilizza, runCurrent() esegue le attività in attesa mantenendo l'ora virtuale corrente. Questo comportamento consente la verifica degli stati intermedi che altrimenti verrebbero ignorati se l'orologio venisse portato a uno stato di inattività.

Lo strumento di pianificazione dei test sottostante utilizzato nell'ambiente di test è esposto. Questo scheduler può essere utilizzato insieme all'API Kotlin runTest per sincronizzare l'orologio di test.

Migrazione a runComposeUiTest

Se utilizzi le API di test di Compose insieme all'API Kotlin runTest, ti consigliamo vivamente di passare a runComposeUiTest.

Approccio precedente

L'utilizzo di createComposeRule in combinazione con runTest crea due orologi separati: uno per Compose e uno per l'ambito della coroutine di test. Questa configurazione può costringerti a sincronizzare manualmente lo strumento di pianificazione dei test.

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

L'API runComposeUiTest esegue automaticamente il blocco di test nel proprio ambito runTest. L'orologio di test è sincronizzato con l'ambiente di Compose, quindi non è più necessario gestire manualmente lo scheduler.

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