Retour au blog
    17 août 2026Tutorial

    Pourquoi votre suite Appium passe au rouge à chaque refonte

    Vos tests passaient vendredi. Le design a livré une nouvelle navigation lundi. Quarante tests sont maintenant rouges, aucune des fonctionnalités sous-jacentes n'est cassée, et quelqu'un passe deux jours à réécrire des sélecteurs.

    Ce n'est pas de l'instabilité — un test instable échoue au hasard. Ceux-ci échouent de façon déterministe, à chaque exécution, jusqu'à ce qu'un humain les modifie. C'est un problème de conception dans la manière dont les tests trouvent les choses, et il existe une liste de correctifs.

    Trier d'abord : sélecteur cassé ou vraie régression ?

    Séparez les deux avant de toucher à quoi que ce soit :

    • Échec sur findElement, la capture montre le bon écran → problème de sélecteur. L'application va bien ; c'est le test qui ne la voit pas.
    • Échec sur une assertion, ou la capture montre le mauvais écran → vraie régression.

    Si votre suite ne prend pas de capture d'écran à l'échec, corrigez cela en premier. Sans elle, tous les échecs se ressemblent et vous retriez les mêmes quarante tests à la main à chaque version.

    Capturez aussi la source de la page, pas seulement la capture d'écran

    Une capture d'écran vous dit à quoi l'écran ressemblait. La source de la page vous dit quels localisateurs existaient à cet instant — c'est-à-dire ce dont vous avez réellement besoin pour écrire le correctif. Joignez-la au rapport et la plupart des sélecteurs cassés se corrigent en une passe, sans relance :

    java
    public class DumpOnFailure implements TestWatcher {
        @Override
        public void testFailed(ExtensionContext ctx, Throwable cause) {
            AppiumDriver driver = DriverHolder.get();
            Allure.addAttachment("page-source.xml", "text/xml", driver.getPageSource());
            Allure.addAttachment("screen.png", new ByteArrayInputStream(
                driver.getScreenshotAs(OutputType.BYTES)));
        }
    }

    Sans elle, la boucle est : lire l'échec → relancer en local → reproduire → inspecter → corriger. Avec elle, la boucle est : ouvrir le rapport → lire l'arbre → corriger. Sur une suite qui casse quarante tests par refonte, cette différence vaut une après-midi entière.

    Sur un rejeu de parcours en échec ici, vous obtenez une version plus forte de la même chose : l'arbre tel qu'enregistré et l'arbre tel qu'il était en direct, plus un score de similarité, pour que la différence pointe directement ce qui a bougé au lieu de vous laisser comparer deux vidages XML à l'œil.

    1. Cessez de dériver les localisateurs à la main

    La plupart des sélecteurs fragiles existent parce que quelqu'un a lu un vidage d'interface et a deviné. Cette supposition est généralement positionnelle :

    java
    // A bet that nobody ever inserts a view above this one
    driver.findElement(By.xpath("//android.widget.LinearLayout[2]/android.widget.Button[1]"));

    RobotActions inspecte l'élément à votre place et renvoie une liste classée de stratégies — id, text, content-desc, sélecteur d'accessibilité, puis XPath en dernier recours. L'inspecteur de l'interface affiche cette liste, et le même classement est accessible à un agent via MCP :

    device_locators_for(udid, text: "Check out") → id: com.myapp:id/checkout_button content-desc: checkout-button text: Check out xpath: //android.widget.Button[@resource-id='com.myapp:id/checkout_button']

    Vous choisissez en haut de cette liste au lieu de dériver depuis le bas. Le classement est le conseil : tout ce qui se trouve sous content-desc est un localisateur que vous modifierez à nouveau.

    « Élément introuvable » veut souvent dire « pas encore atteint par défilement »

    Un piège propre à Android qui envoie les gens droit vers du mauvais XPath : la source de la page ne contient que les nœuds rendus. Un élément sous la ligne de flottaison est réellement absent du vidage, si bien qu'un findElement échoue et qu'on croit le localisateur faux. Il ne l'est pas — l'élément n'a simplement pas encore été disposé.

    La plupart des suites répondent à cela par une boucle maison : balayer vers le haut, sonder, balayer encore, abandonner au bout de N tentatives. Cette boucle est elle-même une source d'instabilité — elle dépend de la distance de balayage, de la hauteur de la liste et de la physique du défilement, qui changent toutes avec l'appareil et avec la refonte.

    device_scroll_to_element fait cela en une seule étape sur Android et iOS : nommez la cible, et l'appareil défile jusqu'à ce qu'elle soit à l'écran, puis agit dessus. Supprimez les boucles.

    Donnez une chaîne de repli aux éléments importants

    Pour la poignée d'éléments dont dépend toute la suite — paiement, connexion, l'appel à l'action principal — ne misez pas sur une stratégie unique. Essayez-les par ordre de stabilité et prenez la première qui se résout :

    java
    private WebElement findFirst(By... strategies) {
        for (By by : strategies) {
            try { return driver.findElement(by); }
            catch (NoSuchElementException ignored) { /* try the next strategy */ }
        }
        throw new NoSuchElementException("no strategy matched: " + Arrays.toString(strategies));
    }
    
    // Ordered exactly like the ranked list above
    WebElement checkout = findFirst(
        AppiumBy.accessibilityId("checkout-button"),
        AppiumBy.id("com.myapp:id/checkout_button"),
        AppiumBy.androidUIAutomator("new UiSelector().text(\"Check out\")"));

    Désormais, une refonte qui renomme l'identifiant de ressource retombe sur l'identifiant d'accessibilité, et un écran qui perd les deux se résout encore par le texte visible. Le test ne passe au rouge que lorsque le bouton a réellement disparu.

    Deux précautions. Journalisez la stratégie qui a fonctionné — un repli silencieux masque la dégradation jusqu'à ce que chaque élément tourne sur son dernier recours. Et placez le texte en dernier, car c'est celui qui casse à la traduction.

    2. Ce qu'il faut demander à l'équipe applicative

    Les localisateurs ne peuvent être que dans la mesure de ce que l'application expose. C'est la partie que la QA ne peut pas faire seule, et la demande est modeste :

    Des identifiants de ressource uniques et stables. Pas button1 dans neuf écrans. Des identifiants dupliqués ramènent le XPath positionnel dans votre suite, c'est-à-dire exactement ce que vous cherchez à éliminer.

    Des identifiants d'accessibilité sur chaque élément interactif. Un seul localisateur fonctionne alors sur les deux plateformes — accessibility_id correspond à contentDescription sur Android et à accessibilityIdentifier sur iOS :

    kotlin
    // Jetpack Compose
    Button(onClick = ::checkout,
        modifier = Modifier.semantics { contentDescription = "checkout-button" }
    ) { Text("Check out") }
    swift
    // SwiftUI
    Button("Check out") { checkout() }.accessibilityIdentifier("checkout-button")
    jsx
    // React Native — testID maps to both platforms
    <Pressable testID="checkout-button" onPress={checkout}><Text>Check out</Text></Pressable>

    Une convention de nommage, arrêtée avant d'en avoir trois cents. Nommez le rôle, pas le design : checkout-button, jamais green-cta-v2. Un nom tiré du design est assuré d'être faux après la prochaine refonte.

    Traitez ces chaînes comme une surface d'API. En renommer une est un changement cassant et passe en revue comme n'importe quel autre. Sans cette règle, elles se font « ranger » et vous revenez au point de départ.

    Le bénéfice secondaire n'est pas mince : un écran que vous ne pouvez pas localiser par identifiant d'accessibilité est généralement un écran difficile à utiliser au lecteur d'écran.

    3. Trouvé n'est pas la même chose qu'atteignable

    Une large catégorie de « le test a cliqué mais rien ne s'est passé » vient d'éléments qui existent dans l'arbre mais ne peuvent pas réellement être touchés :

    • des limites partiellement ou totalement hors de la zone visible
    • un nœud de taille nulle ou replié
    • quelque chose dessiné par-dessus — un pied de page collant, une notification éphémère, une feuille inférieure
    • un élément de classe bouton n'exposant aucune action de clic

    findElement réussit dans chacun de ces cas. L'appui « réussit » aussi. L'échec se manifeste trois étapes plus loin par un écran manquant, et vous allez chercher au mauvais endroit.

    Restreignez votre requête à ce qui est réellement à l'écran — device_elements_in_region renvoie les éléments d'une région plutôt que l'arbre entier — et vérifiez les limites avant d'agir, pas après.

    Il existe un raccourci que la plupart des équipes manquent. Lancez l'audit d'accessibilité sur l'écran et lisez-le comme un rapport d'instabilité, car ces vérifications portent sur les mêmes défauts :

    Constat de l'auditPourquoi votre test est instable
    DuplicateClickableBoundsPlusieurs éléments cliquables partagent des limites identiques — votre localisateur peut désigner le mauvais, de façon non déterministe
    TouchTargetSizeCible tactile sous le minimum recommandé : un appui au centre peut atterrir à côté
    MissingClickActionÉlément de classe bouton n'exposant aucune action de clic — trouvé, touché, sans effet
    SpeakableTextPresentContrôle actionnable sans texte ni description — rien de stable pour le localiser, d'où le recours au XPath

    Cette dernière ligne est exactement la boucle dont parle tout cet article : un contrôle sans nom accessible impose un localisateur positionnel, et un localisateur positionnel casse à la prochaine refonte. Corriger le constat d'audit corrige le test et l'expérience au lecteur d'écran d'un seul geste.

    4. Un seul script, les deux plateformes

    Maintenir des suites iOS et Android parallèles double le coût des refontes sans aucun bénéfice. Deux choses rendent un script unique praticable :

    Utilisez les identifiants d'accessibilité comme stratégie principale. accessibility_id correspond à contentDescription sur Android et à accessibilityIdentifier sur iOS : la même ligne se résout donc sur les deux — à condition que l'équipe applicative utilise la même chaîne des deux côtés. Actez-le dans la convention de nommage, sinon vous obtenez deux conventions et aucune réutilisation.

    Ne branchez que là où les plateformes diffèrent réellement, et gardez le branchement minuscule :

    java
    private static final boolean IOS =
        Platform.fromCapabilities(driver.getCapabilities()) == Platform.IOS;
    
    // Same intent, different system affordance — back navigation
    if (IOS) driver.navigate().back();      // swipe / nav bar
    else     ((AndroidDriver) driver).pressKey(new KeyEvent(AndroidKey.BACK));

    Limitez ces branchements à l'habillage système — retour, autorisations, fermeture du clavier, feuilles de partage. Si vous vous surprenez à brancher sur un flux métier, c'est une divergence produit à signaler plutôt qu'un problème de test à masquer.

    Les verbes de pilotage d'appareil sont ici délibérément symétriques — trouver, attendre, défiler jusqu'à, toucher par libellé existent sur les deux plateformes avec la même sémantique — de sorte que les parties du script qui ne relèvent pas de l'habillage système restent identiques.

    5. Remplacez les pauses par des attentes qui décrivent une condition

    Thread.sleep(3000) est un pari sur le fait que l'appareil est aussi rapide aujourd'hui qu'au moment où vous l'avez écrit. Il échoue au démarrage à froid, sur un réseau lent ou sur un exécuteur de CI chargé — et il gaspille trois secondes à chaque exécution où l'application était prête immédiatement.

    java
    // Explicit wait — fails fast, passes fast
    new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.elementToBeClickable(checkoutButton));
    
    // Fluent wait — poll interval and ignored exceptions under your control
    new FluentWait<>(driver)
        .withTimeout(Duration.ofSeconds(15))
        .pollingEvery(Duration.ofMillis(250))
        .ignoring(NoSuchElementException.class, StaleElementReferenceException.class)
        .until(d -> d.findElement(checkout).isDisplayed());

    Ignorer StaleElementReferenceException compte plus qu'on ne le croit : pendant une animation, l'élément peut être trouvé, puis remplacé en plein sondage. Sans cela, vous obtenez un échec qui ressemble à un élément manquant sans en être un.

    La même condition existe comme étape de premier ordre quand on pilote un appareil ici — device_wait_for_element attend la fin de l'animation au lieu d'en deviner la durée.

    6. Supprimez les interruptions

    Une large part des échecs mobiles « aléatoires » vient de l'OS qui interrompt : une demande d'autorisation, une bannière de mise à jour système, une invitation à noter l'application. Traitez-les structurellement au lieu d'ajouter un try/catch par écran. Les sessions pilotées par navigateur de RobotActions positionnent ceci sur la session sous-jacente :

    json
    { "appium:autoAcceptAlerts": true, "appium:autoDismissAlerts": true }

    Choisissez délibérément — autoAccept accorde les autorisations, ce qui est généralement ce que vous voulez pour une suite de chemin nominal, mais cela signifie aussi que vous ne testez plus le chemin de refus. Gardez au moins un test avec les deux désactivés, qui exerce le comportement en cas d'autorisation refusée.

    7. Enregistrez le parcours, exportez l'objet de page

    Le moyen le plus fiable de cesser d'écrire des localisateurs à la main est de ne pas en écrire. Parcourez le flux une fois sur un appareil réel et exportez-le : vous obtenez un Page Object avec des annotations @FindBy déjà remplies à partir des localisateurs classés, un WebDriverWait, et des utilitaires waitAndClick / waitAndSendKeys — les attentes câblées dès le départ plutôt que rajoutées après la première exécution instable.

    C'est la différence qui mérite d'être mesurée. La plupart des clouds d'appareils vous donnent un écran distant et vous laissent la stratégie de localisation, les attentes et la gestion des interruptions sur les bras. Ici, l'inspecteur, le classement des localisateurs, l'enregistreur et le code exporté forment un même système : les sélecteurs de votre suite sont donc ceux que la plateforme a déjà jugés les plus stables.

    8. Quand il n'y a aucune prise stable

    Parfois il n'y a rien à saisir : une vue de paiement tierce, une WebView que vous ne contrôlez pas, un écran où ajouter des identifiants n'est à la feuille de route de personne. Décrivez alors l'intention plutôt que la structure :

    Ouvre l'application, ajoute un produit au panier et finalise la commande avec la carte enregistrée.

    Un agent IA prend cette histoire utilisateur, pilote un appareil Android ou iOS réel, et trouve les éléments d'après ce qui est à l'écran plutôt que d'après un identifiant interne. Un identifiant renommé ou une nouvelle vue englobante ne change pas l'apparence de l'écran : l'exécution continue de fonctionner — et lorsqu'elle ne peut réellement pas poursuivre, vous obtenez une capture d'écran et une trace de l'endroit où elle s'est arrêtée, pas une NoSuchElementException pointant vers un sélecteur.

    Rien n'a besoin d'être réécrit pour essayer. Pointez votre suite Appium existante vers la grille, gardez tous les tests que vous avez écrits, et laissez l'agent prendre en charge la poignée de parcours qui cassent à chaque refonte.

    La liste de contrôle

    1. Captures d'écran et source de page à l'échec — tout le reste dépend de la capacité à trier sans relancer.
    2. grep -rn "XPath.*\[[0-9]" tests/ — cette liste est votre arriéré, classé par fréquence de modification.
    3. Convertissez les dix premiers en suivant la liste classée de localisateurs, de haut en bas.
    4. Une pull request vers l'application pour les identifiants d'accessibilité manquants, plus la convention de nommage.
    5. Supprimez chaque Thread.sleep et remplacez-le par une attente explicite ou fluide.
    6. Activez le traitement automatique des alertes, et gardez un test qui ne le fait délibérément pas.
    7. Donnez une chaîne de repli à vos cinq éléments les plus sollicités, et journalisez la stratégie qui a fonctionné.
    8. Supprimez les boucles maison de défilement-jusqu'à-visible au profit d'une seule étape de défilement vers l'élément.
    9. Lancez l'audit d'accessibilité sur vos trois écrans les plus instables et traitez DuplicateClickableBounds et SpeakableTextPresent comme des bugs de test, pas comme des agréments d'accessibilité.

    Inspectez un élément sur un appareil réel →

    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.

    👋 Bonjour! Besoin d'aide? Discutez avec nous!

    Discutez avec nous

    En ligne

    Avant de commencer

    Partagez vos coordonnées pour que nous puissions vous recontacter.