Migrer vers les API de test v2

Les versions v2 des API de test Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest, etc.) sont désormais disponibles pour améliorer le contrôle de l'exécution des coroutines. Cette mise à jour ne duplique pas l'intégralité de la surface de l'API. Seules les API qui établissent l'environnement de test ont été mises à jour.

Les API v1 sont obsolètes. Nous vous recommandons vivement de migrer vers les nouvelles API. La migration permet de vérifier que vos tests sont conformes au comportement standard des coroutines et d'éviter les futurs problèmes de compatibilité. Pour obtenir la liste des API v1 obsolètes, consultez Mappages d'API.

Ces modifications sont incluses dans androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+ et androidx.compose.ui:ui-test:1.11.0-alpha03+.

Alors que les API v1 s'appuyaient sur UnconfinedTestDispatcher, les API v2 utilisent StandardTestDispatcher par défaut pour la composition en cours d'exécution. Cette modification aligne le comportement des tests Compose sur les API runTest standards et permet de contrôler explicitement l'ordre d'exécution des coroutines.

Mappages d'API

Lorsque vous passez aux API v2, vous pouvez généralement utiliser Rechercher et remplacer pour mettre à jour les importations de packages et adopter les nouvelles modifications du répartiteur.

Vous pouvez également demander à Gemini d'effectuer une migration vers la version 2 des API de test Compose avec le prompt suivant :

Migrer des API de test v1 vers les API de test v2

Cette invite utilisera ce guide pour migrer vers les API de test v2.

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

Utiliser les requêtes d'IA

Les requêtes d'IA sont destinées à être utilisées dans Gemini dans Android Studio.

Pour en savoir plus sur Gemini dans Studio, consultez https://developer.android.com/studio/gemini/overview.

Utilisez le tableau suivant pour faire correspondre les API v1 obsolètes à leurs remplacements v2 :

Obsolète (v1)

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

Rétrocompatibilité et exceptions

Les API v1 existantes sont désormais obsolètes, mais vous pouvez continuer à utiliser UnconfinedTestDispatcher pour conserver le comportement existant et éviter les modifications destructives.

La seule exception où le comportement par défaut a changé est la suivante :

Le répartiteur de test par défaut utilisé pour exécuter la composition dans la classe AndroidComposeUiTestEnvironment est passé de UnconfinedTestDispatcher à StandardTestDispatcher. Cela affecte les cas où vous créez une instance à l'aide du constructeur ou de la sous-classe AndroidComposeUiTestEnvironment, et où vous appelez ce constructeur.

Changement clé : impact sur l'exécution des coroutines

La principale différence entre les versions 1 et 2 des API réside dans la manière dont les coroutines sont distribuées :

  • API v1 (UnconfinedTestDispatcher) : lorsqu'une coroutine était lancée, elle s'exécutait immédiatement sur le thread actuel, se terminant souvent avant l'exécution de la ligne de code de test suivante. Contrairement au comportement en production, cette exécution immédiate peut masquer par inadvertance de réels problèmes de timing ou des conditions de concurrence qui se produiraient dans une application en direct.
  • API v2 (StandardTestDispatcher) : lorsqu'une coroutine est lancée, elle est mise en file d'attente et ne s'exécute pas tant que le test n'a pas explicitement fait avancer l'horloge virtuelle. Les API de test Compose standards (telles que waitForIdle()) gèrent déjà cette synchronisation. La plupart des tests reposant sur ces API standards devraient donc continuer à fonctionner sans modification.

Cas de non-respect courants et solutions possibles

Si vos tests échouent après la mise à niveau vers la version 2, ils présentent probablement le schéma suivant :

  • Échec : vous lancez une tâche (par exemple, un ViewModel charge des données), mais votre assertion échoue immédiatement, car les données sont toujours dans un état "Loading" (Chargement).
  • Cause : Avec les API v2, les coroutines sont mises en file d'attente plutôt qu'exécutées immédiatement. La tâche a été mise en file d'attente, mais n'a jamais été exécutée avant que le résultat ne soit vérifié.
  • Correction : avancez explicitement dans le temps. Vous devez indiquer explicitement au répartiteur v2 quand exécuter le travail.

Ancienne approche

Dans la version 1, la tâche était lancée et terminée immédiatement. Dans la version 2, le code suivant échoue, car loadData() n'a pas encore été exécuté.

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

Utilisez waitForIdle ou runOnIdle pour exécuter les tâches mises en file d'attente avant l'assertion.

Option 1 : L'utilisation de waitForIdle fait avancer l'horloge jusqu'à ce que l'UI soit inactive, ce qui permet de vérifier que la coroutine s'est exécutée.

viewModel.loadData()

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

assertEquals(Success, viewModel.state.value)

Option 2 : L'utilisation de runOnIdle exécute le bloc de code sur le thread UI une fois que l'UI est devenue inactive.

viewModel.loadData()

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

Synchronisation manuelle

Dans les scénarios impliquant une synchronisation manuelle, par exemple lorsque l'avance automatique est désactivée, le lancement d'une coroutine n'entraîne pas une exécution immédiate, car l'horloge de test est suspendue. Pour exécuter des coroutines dans la file d'attente sans faire avancer l'horloge virtuelle, utilisez l'API runCurrent(). Cela exécute les tâches planifiées pour l'heure virtuelle actuelle.

composeTestRule.mainClock.scheduler.runCurrent()

Contrairement à waitForIdle(), qui fait avancer l'horloge de test jusqu'à ce que l'UI se stabilise, runCurrent() exécute les tâches en attente tout en conservant l'heure virtuelle actuelle. Ce comportement permet de vérifier les états intermédiaires qui seraient ignorés si l'horloge était avancée à un état inactif.

Le planificateur de test sous-jacent utilisé dans l'environnement de test est exposé. Ce planificateur peut être utilisé conjointement avec l'API Kotlin runTest pour synchroniser l'horloge de test.

Migrer vers runComposeUiTest

Si vous utilisez les API de test Compose en même temps que l'API runTest Kotlin, nous vous recommandons vivement de passer à runComposeUiTest.

Ancienne approche

L'utilisation de createComposeRule en association avec runTest crée deux horloges distinctes : une pour Compose et une pour le champ d'application de la coroutine de test. Cette configuration peut vous obliger à synchroniser manuellement le planificateur de tests.

@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 exécute automatiquement votre bloc de test dans son propre champ d'application runTest. L'horloge de test est synchronisée avec l'environnement Compose. Vous n'avez donc plus besoin de gérer le planificateur manuellement.

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