Stabilité lors des tests de grande envergure

La nature asynchrone des applications et des frameworks mobiles rend souvent difficile l'écriture de tests fiables et reproductibles. Lorsqu'un événement utilisateur est injecté, le framework de test doit attendre que l'application ait fini de réagir, ce qui peut aller de la modification d'un texte à l'écran à la recréation complète d'une activité. Lorsqu'un test n'a pas de comportement déterministe, il est instable.

Les frameworks modernes tels que Compose ou Espresso sont conçus pour les tests. Il est donc certain que l'UI sera inactive avant la prochaine action ou assertion de test. C'est ce qu'on appelle la synchronisation.

Tester la synchronisation

Des problèmes peuvent toujours survenir lorsque vous exécutez des opérations asynchrones ou en arrière-plan inconnues du test, telles que le chargement de données à partir d'une base de données ou l'affichage d'animations infinies.

Schéma de flux illustrant une boucle qui vérifie si l'application est inactive avant d'effectuer un test
Figure 1: Synchronisation de test.

Pour améliorer la fiabilité de votre suite de tests, vous pouvez installer un moyen de suivre les opérations en arrière-plan, telles que les ressources d'inactivité Espresso. Vous pouvez également remplacer des modules pour les versions de test que vous pouvez interroger pour l'inactivité ou qui améliorent la synchronisation, tels que TestDispatcher pour les coroutines ou RxIdler pour RxJava.

Schéma illustrant un échec de test lorsque la synchronisation est basée sur l'attente d'un temps fixe
Figure 2: L'utilisation de la mise en veille dans les tests entraîne des tests lents ou irréguliers.

Améliorer la stabilité

Les grands tests peuvent détecter de nombreuses régressions en même temps, car ils testent plusieurs composants d'une application. Ils s'exécutent généralement sur des émulateurs ou des appareils, ce qui signifie qu'ils sont haute fidélité. Bien que les grands tests de bout en bout offrent une couverture complète, ils sont plus sujets à des échecs occasionnels.

Voici les principales mesures que vous pouvez prendre pour réduire l'instabilité:

  • Configurer correctement les appareils
  • Éviter les problèmes de synchronisation
  • Implémenter des nouvelles tentatives

Pour créer de grands tests à l'aide de Compose ou de Espresso, vous démarrez généralement l'une de vos activités et naviguez comme un utilisateur, en vérifiant que l'UI se comporte correctement à l'aide d'assertions ou de tests de capture d'écran.

D'autres frameworks, tels que UI Automator, offrent une portée plus large, car vous pouvez interagir avec l'UI du système et d'autres applications. Toutefois, les tests UI Automator peuvent nécessiter une synchronisation plus manuelle, ce qui les rend moins fiables.

Configurer les appareils

Tout d'abord, pour améliorer la fiabilité de vos tests, vous devez vous assurer que le système d'exploitation de l'appareil n'interrompt pas de manière inattendue l'exécution des tests. Par exemple, lorsqu'une boîte de dialogue de mise à jour du système s'affiche au-dessus d'autres applications ou lorsque l'espace disque disponible est insuffisant.

Les fournisseurs de fermes d'appareils configurent leurs appareils et leurs émulateurs. En général, aucune action n'est requise de votre part. Toutefois, ils peuvent avoir leurs propres directives de configuration pour des cas particuliers.

Appareils gérés par Gradle

Si vous gérez vous-même les émulateurs, vous pouvez utiliser des appareils gérés par Gradle pour définir les appareils à utiliser pour exécuter vos tests:

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

Avec cette configuration, la commande suivante crée une image d'émulateur, démarre une instance, exécute les tests et l'arrête.

./gradlew pixel2api30DebugAndroidTest

Les appareils gérés par Gradle contiennent des mécanismes de nouvelle tentative en cas de déconnexion de l'appareil et d'autres améliorations.

Éviter les problèmes de synchronisation

Les composants qui effectuent des opérations en arrière-plan ou asynchrones peuvent entraîner des échecs de test, car une instruction de test a été exécutée avant que l'UI ne soit prête. À mesure que le champ d'application d'un test augmente, le risque qu'il devienne instable augmente également. Ces problèmes de synchronisation sont une source principale d'instabilité, car les frameworks de test doivent déduire si le chargement d'une activité est terminé ou s'il doit attendre plus longtemps.

Solutions

Vous pouvez utiliser les ressources inutilisées d'Espresso pour indiquer quand une application est occupée, mais il est difficile de suivre chaque opération asynchrone, en particulier dans les tests de bout en bout très volumineux. De plus, les ressources inutilisées peuvent être difficiles à installer sans polluer le code en cours de test.

Au lieu d'estimer si une activité est occupée ou non, vous pouvez faire en sorte que vos tests attendent que des conditions spécifiques soient remplies. Par exemple, vous pouvez attendre qu'un texte ou un composant spécifique s'affiche dans l'UI.

Compose dispose d'une collection d'API de test dans ComposeTestRule pour attendre différents outils de mise en correspondance:

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

Et une API générique qui accepte n'importe quelle fonction qui renvoie un booléen:

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

Exemples d'utilisation :

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

Mécanismes de nouvelle tentative

Vous devez corriger les tests instables, mais parfois les conditions qui les font échouer sont si improbables qu'elles sont difficiles à reproduire. Bien que vous deviez toujours suivre et corriger les tests instables, un mécanisme de nouvelle tentative peut aider à maintenir la productivité des développeurs en exécutant le test plusieurs fois jusqu'à ce qu'il réussisse.

Les tentatives doivent être effectuées à plusieurs niveaux pour éviter les problèmes, par exemple:

  • La connexion à l'appareil a expiré ou a été perdue
  • Échec d'un seul test

L'installation ou la configuration des nouvelles tentatives dépend de vos frameworks de test et de votre infrastructure, mais les mécanismes courants incluent les suivants:

  • Règle JUnit qui réexécute un test un certain nombre de fois
  • Une action ou une étape de nouvelle tentative dans votre workflow CI
  • Système permettant de redémarrer un émulateur lorsqu'il ne répond pas, comme les appareils gérés par Gradle.