Tests mobiles instables : un ordre de tri qui converge vraiment
La plupart des équipes ne corrigent pas l'instabilité. Elles ajoutent une relance automatique, regardent la suite passer au vert et passent à autre chose — et six mois plus tard, plus personne ne fait confiance à un build rouge, parce que la moitié d'entre eux sont du bruit et que distinguer laquelle relève du pile ou face.
Si cela ne converge jamais, ce n'est pas faute d'efforts. C'est que « instable » désigne cinq problèmes complètement différents, aux correctifs complètement différents, et que les équipes déboguent celui auquel elles ont pensé en premier.
Voici un ordre qui, lui, se termine.
Étape 0 : mesurer, pas se souvenir
On ne peut pas trier à partir d'impressions. Avant toute chose, obtenez un chiffre par test : le taux d'échec sur les 50 dernières exécutions, à code inchangé.
Si vous ne l'avez pas, le moyen le moins cher de l'obtenir est une tâche nocturne qui exécute la
suite cinq fois contre main et ajoute les résultats à un CSV. Une semaine vous donne 35 points
de mesure par test, ce qui suffit pour classer.
Deux choses apparaissent immédiatement, et elles valent à elles seules la semaine :
- En général, moins de 10 % des tests produisent plus de la moitié des échecs instables. Corrigez ces cinq tests et la suite paraît transformée. Sans les données, vous auriez consacré ce temps au test dont quelqu'un s'est plaint le plus fort.
- Certains tests « instables » ne le sont pas du tout : ils échouent 100 % du temps sur un appareil ou une version d'OS et passent partout ailleurs. Ce n'est pas de l'instabilité, c'est un bug non déclaré qui se déguise — et c'est l'élément de plus grande valeur de la liste.
Classez par taux_d_échec × exécutions_par_jour. Traitez de haut en bas. Arrêtez de chercher des
candidats dans Slack.
Étape 1 : est-ce le test, ou est-ce l'application ?
Avant de conclure à un bug de test, prenez le premier de la liste et exécutez-le 20 fois isolément sur un seul appareil.
- Il échoue à un taux comparable en isolation → c'est le test, ou un vrai bug applicatif. Continuez à l'étape 2.
- Il passe 20 fois sur 20 en isolation et échoue dans la suite → ce sont les tests autour de lui qui interfèrent. Sautez à l'étape 3.
Cette seule question coupe le travail en deux, prend vingt minutes, et est omise à peu près universellement.
Étape 2 : le problème de synchronisation (l'essentiel de l'instabilité est ici)
Presque toute l'instabilité d'un test isolé est une course entre votre test et le rendu de l'application, et presque toute vient des trois mêmes schémas.
Dormir au lieu d'attendre. sleep(2) est un pari sur le fait que l'application est prête en
moins de deux secondes sur tous les appareils, tous les réseaux, toutes les charges de CI. Il
passe sur votre portable dernier cri et échoue sur un Android de milieu de gamme en limitation
thermique. Chaque pause fixe est un test instable à retardement.
Attendre la présence, agir sur l'interactivité. L'élément existe dans l'arbre bien avant d'être prêt. Un bouton affiché mais encore en cours d'animation avale le tap et signale une réussite — le tap a bien atterri, il n'a simplement rien fait, et vous échouez trois assertions plus loin sur un écran qui n'a jamais changé. Attendez qu'il soit activé et stable, pas simplement présent.
Attendre la mauvaise chose. Le grand classique : attendre l'apparition d'un indicateur de chargement alors que, sur une réponse rapide, il apparaît et disparaît avant que l'intervalle de scrutation ne l'attrape — et votre attente expire sur un écran déjà chargé avec succès.
// Flaky: passes when the spinner is slow, times out when the app is fast
wait.until(visibilityOfElementLocated(By.id("loading_spinner")));
wait.until(invisibilityOfElementLocated(By.id("loading_spinner")));
// Stable: wait for the post-condition, not the transition
wait.until(elementToBeClickable(By.id("results_list")));La règle générale : vérifiez l'état que vous voulez, jamais la transition qui y mène. Les transitions sont des courses par définition ; les états finaux, non.
Un dernier point qui n'apparaît que sur du matériel réel : les animations. Un écran qui a fini de
charger mais glisse encore en place remettra votre tap au pixel qui se trouve sous le doigt en
plein vol. Désactiver les animations système sur l'appareil (appium:disableWindowAnimation, ou
les réglages d'échelle des options développeur) supprime toute une classe d'échecs et accélère la
suite par-dessus le marché.
Étape 3 : la fuite d'état (celle qui grandit avec votre suite)
Si le test passe seul et échoue en groupe, quelque chose d'extérieur modifie ce qu'il voit.
Parcourez les surfaces partagées dans cet ordre — du plus au moins fréquent :
- L'état de l'appareil. Connexions en cache, autorisations accordées, écrans d'accueil passés, une notification encore affichée, un clavier resté ouvert, l'application laissée en arrière-plan. Le test 40 hérite de ce que le test 39 a laissé.
- L'état du back-end. Le compte dont un autre test vient de vider le panier ; l'entité qu'un autre test a renommée ; le drapeau de fonctionnalité que quelqu'un a basculé.
- Les identifiants partagés. Un seul compte utilisé par des partitions parallèles, où une nouvelle session invalide l'ancienne et où un test sans rapport se retrouve renvoyé à l'écran de connexion.
- L'heure et le calendrier. Un test qui passe sauf autour de minuit, ou le dernier jour du mois, ou sur un exécuteur de CI en UTC quand l'assertion supposait l'heure locale.
- L'accumulation de données. Le test 1 crée un élément, si bien que l'assertion « la liste doit afficher 3 éléments » du test 12 compte discrètement depuis une semaine.
Le correctif a toujours la même forme, et c'est rarement « nettoyer après coup » : faites en sorte que chaque test crée ce qu'il vérifie. Un nettoyage en fin de test échoue précisément quand le test échoue — c'est-à-dire quand la fuite compte le plus — si bien que le test suivant hérite du désordre de l'exécution déjà partie de travers, et qu'un vrai échec en cascade en produit six faux.
Étape 4 : l'environnement (à écarter avant de réécrire quoi que ce soit)
Si les étapes 2 et 3 n'ont rien donné, arrêtez de modifier le test et regardez où il s'est exécuté.
- La santé de l'appareil. Une batterie faible bride le processeur. Un stockage plein fait échouer les installations et les captures d'écran de façons créatives. La limitation thermique, après une heure d'exécutions continues, ralentit suffisamment tout pour faire sauter des délais qui tenaient très bien à la première exécution.
- Le réseau. Une API de recette dont le p99 est à huit secondes produira une instabilité qu'aucun correctif côté test ne pourra supprimer. Journalisez les temps de requête ; si le p99 bouge en même temps que les échecs, le correctif est dans le back-end, pas dans la suite.
- Les limites de débit. Les partitions parallèles multiplient votre débit de requêtes. Un 429 se manifeste généralement dans l'application par un écran d'erreur générique, donc cela se lit comme un bug applicatif.
- Les interruptions de l'OS. Une bannière de mise à jour, une alerte « stockage presque plein », un message de l'opérateur qui atterrit au milieu de votre parcours. Plus rare qu'on ne le suppose, mais réellement aléatoire quand cela arrive — ce qui en fait la catégorie la plus déroutante à déboguer à partir d'une seule trace d'exécution.
C'est aussi là que des appareils inconstants se manifestent en tests inconstants. Si deux exécutions du même test tombent sur deux téléphones configurés différemment — langue différente, niveau de correctif différent, restes d'état différents — on reproche au test la variance de l'environnement. Travailler sur un appareil dédié et stable d'une exécution à l'autre ne corrige pas un test mal écrit, mais cela supprime la variable, ce qui est la condition pour pouvoir conclure quoi que ce soit à l'étape 1.
Étape 5 : c'est un vrai bug
Certains tests instables ont raison. Ils attrapent une vraie course dans l'application — une requête qui se résout parfois après la disparition de l'écran, un cache occasionnellement périmé, un écouteur enregistré deux fois.
Le signe distinctif : l'échec ne se produit pas sur un findElement, la capture d'écran
montre un écran plausible, et c'est l'application qui se comporte mal plutôt que le test qui
regarde au mauvais endroit. Supprimer ou relancer ce test-là, c'est supprimer un rapport de bug
qui se reproduit.
Ce qu'il faut capturer pour que le tri soit seulement possible
Presque rien de ce qui précède ne se déduit d'une trace d'exécution. À chaque échec, capturez :
- Une capture d'écran au moment de l'échec — sépare instantanément « mauvais écran » de « bon écran, mauvais sélecteur ».
- L'arbre des éléments à cet instant — vous dit quels localisateurs existaient réellement, ce qui est exactement ce qu'il faut pour écrire le correctif.
- La vidéo de l'exécution — le seul moyen d'attraper un dialogue apparu puis disparu, ou un tap tombé en pleine animation.
- Les journaux de l'appareil et les requêtes réseau — transforment « l'application a affiché une erreur » en « l'API a renvoyé 429 ».
- Quel appareil, quelle version d'OS, quel build — pour que le motif « n'échoue que sur un appareil » puisse seulement apparaître.
Sans cela, vous relancez en local pour reproduire, ce qui, pour des tests réellement instables, est le plus gros gouffre à temps du processus : vous essayez de reproduire quelque chose qui survient une fois sur huit, et chaque tentative coûte une préparation de suite complète.
À propos des relances
Les relances sont un outil de mesure, pas un correctif. Configurez-les pour qu'un test qui passe à la relance reste signalé distinctement d'un test passé du premier coup — la plupart des exécuteurs le permettent, et c'est toute la différence entre une politique de relance et un enfouissement.
Traitez ensuite le nombre de « passé à la relance » comme votre métrique d'instabilité, et tenez-la dans un budget. Un test qui a besoin de relances est sur un compte à rebours, pas sur une liste.
La règle de la quarantaine
La quarantaine ne fonctionne qu'avec une date d'expiration. Sans elle, c'est un cimetière : des tests que personne n'exécute, que personne ne supprime, et auxquels personne ne se fie.
Une politique qui survit au contact d'une vraie équipe :
- Un test au-dessus de votre budget d'instabilité passe en quarantaine — il s'exécute encore, mais ne bloque plus le build.
- On lui attribue un responsable et un délai de deux semaines, tous deux écrits noir sur blanc.
- Au bout de deux semaines, il est soit corrigé, soit supprimé.
La suppression est une issue légitime. Un test auquel personne ne se fie et que personne ne corrige a une valeur négative : il coûte du temps d'exécution, il coûte de l'attention au tri, et il entraîne tout le monde à ignorer le rouge.
Prêt à tester sur de vrais appareils ?
Connectez-vous avec Google ou GitHub et accédez à de vrais appareils iOS et Android depuis votre navigateur — gratuit à l'essai.