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. Il s'agit de 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.
Pour augmenter la fiabilité de votre suite de tests, vous pouvez installer un moyen de suivre les opérations en arrière-plan, comme les ressources inactives d'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.
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 sur 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.
Les principales mesures que vous pouvez prendre pour réduire la fragilité sont les suivantes:
- Configurer correctement les appareils
- Éviter les problèmes de synchronisation
- Implémenter des nouvelles tentatives
Pour créer des tests volumineux à l'aide de Compose ou d'Espresso, vous démarrez généralement l'une de vos activités et naviguez comme le ferait 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, élargissent le champ d'application, 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'interface utilisateur 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 d'inactivité 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 très volumineux de bout en bout. En outre, les ressources inactives peuvent être difficiles à installer sans polluer le code 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 avoir lieu à plusieurs niveaux pour éviter les problèmes, par exemple:
- Le délai de connexion à l'appareil a expiré ou a été perdue
- Échec du test unique
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.