Esegui la migrazione alle API di 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 ritirate, 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.

Mapping delle API

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 la migrazione alla v2 delle API di test di Compose 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 seguente tabella per mappare le API v1 ritirate alle relative sostituzioni v2:

Deprecato (v1)

Sostituzione (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 ritirate, ma continuano a utilizzare UnconfinedTestDispatcher per mantenere il comportamento esistente ed evitare modifiche che potrebbero causare problemi.

La seguente è 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 condizioni di competizione 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 Compose standard (come waitForIdle()) gestiscono già questa sincronizzazione, quindi la maggior parte dei test che si basano su queste API standard dovrebbe continuare a funzionare senza modifiche.

Errori comuni e come correggerli

Se i test non riescono dopo 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 riesce 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 fosse 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 funziona 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 UI 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 non 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 scheduler di test sottostante utilizzato nell'ambiente di test è esposto. Questo scheduler può essere utilizzato insieme all'API Kotlin runTest per sincronizzare l'orologio di test.

Esegui la 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 insieme a 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 composizione, 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()
    }
}