Formation Clean QA & Patterns – Guide complet

Manifeste, architecture et patterns pour une QA robuste et durable

Introduction & Contexte

La Qualité Logicielle a longtemps été perçue comme une activité périphérique, presque secondaire par rapport au développement. Historiquement, le test était vu comme une étape post-développement, une phase coûteuse et souvent sacrifiée lorsque les délais de mise en production devenaient trop serrés. Cette vision réductrice a contribué à faire de la QA un domaine sous-estimé, parfois limité à des vérifications manuelles répétitives ou à des campagnes de tests peu structurées.

Pourtant, l’évolution des systèmes a changé la donne : l’avènement des microservices, la généralisation du mobile, la complexité du retail et la montée en puissance du cloud ont fait de la QA une discipline clé, au même titre que le développement ou l’architecture logicielle. Dans un environnement distribué et en constante évolution, la qualité n’est plus un luxe, mais une condition de survie.

« Un bug critique en production ne se mesure pas seulement en coûts techniques, mais aussi en perte de confiance des utilisateurs et en impact sur l’image de l’entreprise. »
Note : Clean QA ne remplace pas les méthodologies existantes comme TDD ou BDD. Il les complète, en apportant un cadre architectural et philosophique spécifique à la QA, garantissant que les tests ne soient pas de simples scripts techniques mais de véritables actifs stratégiques.

Aujourd’hui, force est de constater que la majorité des frameworks de tests souffrent de maux récurrents :

  • Spaghetti code : des tests écrits dans l’urgence, sans structure claire.
  • Duplication : les mêmes étapes recopiées dans des dizaines de scripts.
  • Dépendances implicites : un test dépend d’un autre sans que ce soit explicité.
  • Tests instables (flaky) : un jour verts, un jour rouges, sans changement de code.
Attention : ces symptômes ne sont pas des fatalités. Ils résultent d’un manque de principes clairs et d’une architecture mal pensée en amont. Un framework de tests peut devenir aussi complexe qu’une application métier s’il n’est pas construit avec rigueur.

C’est précisément dans ce contexte qu’est né le concept de Clean QA : une approche pragmatique et durable, inspirée de Clean Code et du Software Craftsmanship, mais appliquée spécifiquement au domaine du test automatisé. L’ambition est simple :

  • Pérennité : les tests doivent survivre aux refactorings et aux changements d’architecture.
  • Lisibilité : un test doit pouvoir être compris en quelques secondes, même par un non-technicien.
  • Valeur ajoutée : chaque test doit démontrer son utilité métier, et non pas seulement cocher une case de couverture.

Exemple concret

Prenons une équipe e-commerce qui gère 2000 tests automatisés Selenium. Faute d’architecture, chaque test contient ses propres identifiants CSS, ses propres données en dur, et ses propres fonctions utilitaires. Résultat : lorsqu’une classe CSS change dans l’application, 300 tests cassent en même temps.

En adoptant les principes de Clean QA, on centralise ces sélecteurs, on factorise les données, et on applique des patterns comme Page Object ou Screenplay. Résultat : un changement technique n’impacte que 2 ou 3 fichiers centraux, et non des centaines de scripts.

Clean QA se positionne donc comme une discipline à part entière : ni purement technique, ni purement méthodologique. C’est une philosophie appliquée, qui cherche à redonner aux tests leur rôle premier : sécuriser, documenter et accompagner l’évolution des systèmes avec efficacité.

Objectifs pédagogiques

  • Concevoir des frameworks de tests robustes et évolutifs.
  • Appliquer les patterns de conception au test automatisé.
  • Instaurer un socle de bonnes pratiques QA partagées.
  • Comprendre les anti-patterns et savoir les éviter.
  • Développer une culture QA centrée sur la valeur métier.
À l’issue de cette formation, vous serez capables de transformer un framework fragile en une architecture durable, lisible et maintenable, quelle que soit la technologie utilisée.

Philosophie Clean QA

La philosophie Clean QA est avant tout un état d’esprit. Elle repose sur l’idée que les tests automatisés ne sont pas de simples scripts jetables, mais des actifs stratégiques de l’entreprise. À l’image du mouvement Clean Code, Clean QA propose un ensemble de valeurs et de principes permettant de guider les équipes vers des choix plus réfléchis, plus durables, et orientés vers la lisibilité, la simplicité et l’adaptabilité.

Là où certaines approches de tests se focalisent uniquement sur la couverture ou la vitesse d’exécution, Clean QA met en avant la notion de valeur métier. Autrement dit, un test bien conçu n’est pas seulement celui qui passe rapidement, mais celui qui démontre clairement sa pertinence pour le produit et pour l’équipe.

Note : Clean QA n’est pas une méthodologie figée. Ce n’est ni un framework, ni un outil. C’est un cadre de pensée, un ensemble de principes qui peuvent être appliqués à n’importe quel contexte, quel que soit le langage, l’outil ou la méthodologie (Agile, DevOps, etc.).

Le manifeste Clean QA

Le manifeste s’inspire directement du manifeste Agile et du mouvement Craftsmanship. Il propose une série de valeurs contrastées, sous la forme « nous privilégions ceci plutôt que cela ». Ces contrastes permettent de rappeler que chaque décision technique a un impact sur la pérennité des tests.

Exemple de valeurs Clean QA

  • Lisibilité du test > Astuce technique.
  • Valeur métier > Couverture brute.
  • Framework modulaire > Monolithe complexe.
  • Prévention de la dette QA > Refactoring tardif.

Ces valeurs ne signifient pas que la couverture ou la performance n’ont aucune importance, mais qu’elles ne doivent jamais se faire au détriment de la lisibilité et de la valeur métier. Un test obscur mais rapide est une bombe à retardement : il deviendra un fardeau pour l’équipe dès qu’il faudra l’analyser ou le corriger.

La carte des valeurs

La carte des valeurs Clean QA peut être représentée comme une boussole. Elle aide les équipes à arbitrer leurs choix quotidiens. Lorsqu’un dilemme se présente — par exemple, entre écrire un test ultra-optimisé mais illisible, ou un test plus simple mais clair — la carte invite à privilégier le second.

En pratique : Si un test est compréhensible par un Product Owner ou un développeur junior, c’est un bon indicateur de lisibilité. S’il nécessite 15 minutes d’explication, il n’est probablement pas Clean QA.

Comparaison avec d’autres approches

Approche Focus principal Limite Apport du Clean QA
TDD (Test Driven Development) Conception par le test unitaire Très technique, peu de vision métier Apporte la lisibilité et la structure côté framework
BDD (Behavior Driven Development) Scénarios lisibles par les métiers Peut générer des steps mal factorisés Applique des patterns pour éviter la dette QA
TestOps / CI-CD Industrialisation et exécution massive Souvent au détriment de la maintenabilité Rappelle que chaque test doit être durable

Anti-valeurs à éviter

  • Écrire des tests uniquement pour « remplir la pyramide QA ».
  • Optimiser la vitesse au détriment de la compréhension.
  • Multiplier les frameworks hétérogènes sans cohérence.
  • Laisser s’accumuler la dette technique en pensant « on refactorisera plus tard ».
Attention : Les anti-valeurs ne sont pas des erreurs ponctuelles, mais des tendances qui, si elles persistent, détruisent la crédibilité de la QA. Un test qui échoue souvent ou qui est ignoré devient invisible… et donc inutile.

En résumé, le Clean QA n’est pas une règle stricte à appliquer à la lettre, mais un guide de décision. Il invite les équipes à se poser la question : « Ce choix rend-il nos tests plus lisibles, plus durables, plus utiles ? ». Si la réponse est non, alors il est probablement temps de revoir l’approche.

Patterns appliqués aux tests

L’un des piliers de Clean QA est l’application des design patterns au monde du test. Ces schémas de conception, bien connus des développeurs, trouvent une pertinence toute particulière dans la construction de frameworks de QA robustes. Ils apportent clarté, réutilisabilité et modularité, permettant d’éviter la dette technique et de réduire la fragilité des tests.

« Sans patterns, les tests sont du code comme les autres… mais souvent en pire. Avec les patterns, ils deviennent un véritable langage de validation du système. »

Page Object Pattern

Le Page Object Pattern est sans doute le plus connu dans le domaine des tests UI. Son objectif est simple : séparer la logique d’interaction technique (sélecteurs, clics, saisies) des scénarios métier. Chaque page ou composant de l’application est représenté par une classe dédiée, ce qui permet d’éviter la duplication et d’améliorer la lisibilité.

Exemple simplifié en Java

class LoginPage {
  void saisirIdentifiant(String user) { 
    driver.findElement(By.id("username")).sendKeys(user);
  }
  void saisirMotDePasse(String pwd) { 
    driver.findElement(By.id("password")).sendKeys(pwd);
  }
  void valider() { 
    driver.findElement(By.id("login")).click();
  }
}
    

Avec ce pattern, les tests eux-mêmes deviennent plus expressifs :

LoginPage login = new LoginPage();
login.saisirIdentifiant("alice");
login.saisirMotDePasse("secret");
login.valider();
  
Note : Le Page Object est efficace pour les interfaces stables, mais peut devenir lourd si chaque micro-composant est isolé dans une classe. D’où la nécessité de le combiner avec d’autres patterns.

Screenplay Pattern

Le Screenplay Pattern, popularisé par la communauté Serenity BDD, va plus loin que le Page Object. Ici, les tests sont décrits comme une suite d’actions réalisées par un acteur. Chaque acteur possède des compétences (navigate, click, remember…), et exécute des tâches (login, search, purchase). Cela rapproche fortement le test du langage métier.

Exemple de scénario en pseudo-code

Actor alice = new Actor("Alice");

alice.can(BrowseTheWeb.with(driver));

alice.attemptsTo(
  Login.withCredentials("alice", "secret"),
  Search.forProduct("Laptop"),
  AddToCart.theFirstResult()
);
    

Ici, le test se lit presque comme une user story. La lisibilité est exceptionnelle, et les détails techniques (locators, API calls) sont encapsulés dans les tâches.

Avantage majeur : Le Screenplay Pattern favorise la composition. Les mêmes tâches peuvent être réutilisées dans différents scénarios sans duplication.
Attention : Sa mise en place demande une discipline stricte. Sans rigueur, on risque de recréer du spaghetti code… mais distribué dans des dizaines de classes.

Factory & Builder

Les tests automatisés reposent autant sur les données que sur les actions. Or, la duplication de datasets (articles, utilisateurs, commandes) est une source massive de dette technique. C’est là que les patterns Factory et Builder interviennent.

Exemple : génération d’un utilisateur de test

User user = UserBuilder.aUser()
             .withName("Alice")
             .withRole("Admin")
             .withEmail("alice@test.com")
             .build();
    

Avec ce pattern, créer de nouvelles données devient simple et expressif. Plus besoin de dupliquer des JSON ou des CSV, tout est centralisé. Le Factory peut fournir des objets standards (ex. un “utilisateur par défaut”), tandis que le Builder permet de les personnaliser.

Strategy & Observer

Certains tests nécessitent d’adapter leur comportement selon le contexte. Par exemple, une validation peut se faire via API en préproduction, mais via OCR en environnement POS. Le pattern Strategy permet de définir plusieurs stratégies de validation et de choisir dynamiquement laquelle utiliser.

Exemple simplifié : validation dynamique

interface ValidationStrategy {
  boolean validate(Order order);
}

class ApiValidation implements ValidationStrategy {
  boolean validate(Order order) { return api.check(order); }
}

class OcrValidation implements ValidationStrategy {
  boolean validate(Order order) { return ocr.read(order.ticket); }
}

// Utilisation
ValidationStrategy strategy = new OcrValidation();
assertTrue(strategy.validate(order));
    

Le pattern Observer, quant à lui, est utile pour écouter les événements d’un test : capture de logs, screenshots, métriques de performance. Cela permet de brancher des outils de reporting ou d’analyse sans modifier la logique des tests.

Astuce : Brancher un Test Observer qui capture automatiquement un screenshot en cas d’erreur permet d’éviter des heures de debug.

Comparaison des patterns

Pattern Avantages Limites Quand l’utiliser
Page Object Simplicité, encapsulation des sélecteurs Devient lourd sur des projets complexes Tests UI simples ou projets débutants
Screenplay Lisibilité, réutilisabilité, langage métier Courbe d’apprentissage, beaucoup de classes Frameworks complexes, besoin de lisibilité
Factory / Builder Factorisation des données, flexibilité Demande une bonne conception du modèle Gestion massive de datasets
Strategy Adaptabilité, extensibilité Complexité accrue si mal géré Tests multi-contextes (API/UI/OCR)
Observer Monitoring, reporting, audit Risque de bruit excessif dans les logs Frameworks avec exigence forte de traçabilité

En résumé

L’application de ces patterns transforme radicalement un framework QA. Au lieu d’être un simple assemblage de scripts, il devient une architecture pensée, robuste et lisible. Chaque pattern répond à un besoin précis : structurer, réutiliser, adapter, monitorer. Utilisés ensemble, ils forment le socle de la démarche Clean QA.

Architecture Clean QA

Construire un framework de test automatisé, c’est comme construire une application critique : si les fondations sont fragiles, tout l’édifice s’écroule. L’architecture Clean QA part du principe que les tests doivent être conçus avec le même niveau de rigueur que le code de production. Pas de raccourcis, pas de bricolage : une stratification claire, une séparation des responsabilités, et une extensibilité naturelle.

« Les tests sont du code. Le nier, c’est condamner son framework QA à répéter les erreurs du passé. » — Adapté de Robert C. Martin (Clean Code)

Pourquoi une architecture ?

Beaucoup de projets QA échouent car ils ne considèrent pas le framework comme un logiciel à part entière. Résultat :

  • Tests dispersés sans organisation claire.
  • Dépendances implicites entre cas de test.
  • Données dupliquées et non maintenues.
  • Couplage fort avec l’outil (Selenium, Katalon, Cypress…).
Attention : sans architecture, la QA devient une dette. Plus on ajoute de tests, plus on accélère la chute de la maintenabilité.

Les couches principales de Clean QA

Un framework Clean QA repose sur une architecture en couches, chacune ayant une responsabilité unique.

1. Le Core

Le Core est la fondation. Il centralise la gestion des interactions génériques avec le système testé. Ici vivent les abstractions qui masquent les détails techniques.

public class ScreenOperationsManager {
    public void clickOn(String locator) { ... }
    public void enterText(String locator, String value) { ... }
    public String readText(String locator) { ... }
}
  

Avantages :

  • Un seul endroit à modifier si l’outil d’automatisation change.
  • Réduction des duplications (clic, saisie, vérification toujours centralisés).
  • Uniformité des logs et des erreurs.

2. Les Steps

Les Steps incarnent le métier. Chaque méthode correspond à une action fonctionnelle, en s’appuyant exclusivement sur le Core. Exemple :

public class CheckoutSteps {
    private final ScreenOperationsManager screen;

    public CheckoutSteps(ScreenOperationsManager screen) {
        this.screen = screen;
    }

    public void ajouterArticleAuPanier(String ean) {
        screen.enterText("#searchBar", ean);
        screen.clickOn("#btnAdd");
    }
}
  
Règle d’or : un Step ne doit jamais contenir de sélecteur brut. Il manipule uniquement des abstractions du Core.

3. Les Datasets

Les données sont le carburant des tests. Mal gérées, elles deviennent la source principale d’instabilité. En Clean QA, elles sont centralisées dans une couche dédiée : Datasets.

Exemple avec Builder

User user = UserBuilder.aUser()
             .withName("Alice")
             .withRole("Admin")
             .withEmail("alice@test.com")
             .build();
    

On distingue :

  • Factories : fournissent des objets par défaut.
  • Builders : permettent de créer des variations personnalisées.
  • Sources dynamiques : API, BDD, fichiers JSON/CSV.

4. Les Utils

La boîte à outils transversale du framework. On y trouve les helpers (Logger, DateUtils, JsonParser…), réutilisables partout.

public class Logger {
    public static void info(String message) { ... }
    public static void error(String message, Exception e) { ... }
}
  

L’architecture hexagonale appliquée à la QA

Le modèle Ports & Adapters s’applique parfaitement à la QA. Les scénarios (le cœur métier) n’interagissent jamais directement avec Selenium, Appium ou une API. Ils passent par des ports, et les adapters traduisent la demande.

Exemple

interface PaymentPort {
    boolean process(Payment payment);
}

class ApiPaymentAdapter implements PaymentPort {
    public boolean process(Payment payment) { ... }
}

class UiPaymentAdapter implements PaymentPort {
    public boolean process(Payment payment) { ... }
}
    
Avec cette approche, changer de canal (API vs UI) ne nécessite pas de réécrire les tests : il suffit de brancher un autre adapter.

Comparaison : classique vs Clean QA

Aspect Framework classique Framework Clean QA
Structure Scripts dispersés, sélecteurs en dur Modules stratifiés, responsabilités claires
Maintenance Chaque changement casse des dizaines de tests Un changement se gère dans le Core/Datasets
Évolutivité Ajouts douloureux et coûteux Facile à enrichir par ajout de Steps/Adapters
Lisibilité Scénarios longs et techniques Steps proches du langage métier

Cas pratique : refactorisation d’un test spaghetti

Exemple d’un test classique écrit à la hâte :

// Mauvais exemple
driver.findElement(By.id("username")).sendKeys("alice");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.id("login")).click();
driver.findElement(By.id("search")).sendKeys("laptop");
driver.findElement(By.id("add")).click();
  

Version Clean QA après refactorisation :

// Steps + Core
LoginSteps login = new LoginSteps(screen);
CheckoutSteps checkout = new CheckoutSteps(screen);

login.seConnecter("alice", "secret");
checkout.ajouterArticleAuPanier("laptop");
  
Résultat : lisible par n’importe quel membre de l’équipe, et isolé des changements techniques.

Check-list Clean QA

Pour savoir si votre architecture est vraiment Clean QA, posez-vous ces questions :

  • Les sélecteurs sont-ils centralisés (Core) ?
  • Les Steps sont-ils 100% métier ?
  • Les données sont-elles factorisées (Factories/Builders) ?
  • Les dépendances externes sont-elles isolées par des Adapters ?
  • Les Utils sont-ils découplés et réutilisables ?
  • Peut-on changer d’outil (Selenium ➝ Playwright) sans réécrire tous les tests ?
Si la réponse est “non” à plus de 2 questions, votre framework n’est probablement pas Clean QA… et la dette grandit.

En résumé

L’architecture Clean QA n’est pas une option, c’est un prérequis. Elle transforme la QA d’un centre de coûts fragile en un levier stratégique. En appliquant les principes de modularité, de stratification et d’isolation via Ports & Adapters, on obtient des tests :

  • Robustes : ils survivent aux refactorings.
  • Lisibles : ils parlent le langage du métier.
  • Évolutifs : ils s’adaptent aux nouveaux besoins sans tout casser.

Bonnes pratiques Clean QA

Les bonnes pratiques constituent le garde-fou d’un framework Clean QA. Sans elles, même la meilleure architecture finit par se dégrader. Elles ne sont pas des options : ce sont des règles de survie. L’histoire de l’ingénierie logicielle montre que les projets échouent rarement par manque de fonctionnalités, mais très souvent par accumulation de dette technique. En QA, cette dette se manifeste par des tests instables, illisibles, coûteux à maintenir.

« Le code pourri fait échouer un projet. Les tests pourris font échouer une entreprise. »

1. Convention de nommage

Le nommage est la première barrière contre l’opacité. Un test doit être lu comme une **phrase claire**, reflétant le comportement validé. Bannissez les abréviations, les identifiants cryptiques, les versions numérotées.

// Mauvais
@Test
public void test_45_v2() { ... }

// Bon
@Test
public void authentification_echoue_si_mdp_invalide() { ... }
  
Astuce : utilisez des verbes d’action (“valider”, “rejeter”, “créer”) et des compléments explicites. Les noms deviennent alors auto-documentés.

2. Assertions claires

Un test sans assertion n’est pas un test, c’est une simulation. Les assertions doivent être explicites et contextuelles.

// Mauvais
click("#submit");

// Bon
click("#submit");
assertTrue(isVisible("#confirmationMessage"),
          "Le message de confirmation doit apparaître après la soumission.");
  

Les assertions trop génériques (“assertTrue(page.contains('OK'))”) sont à proscrire. Elles créent de faux positifs et dégradent la confiance.

3. Gestion des données

Les données sont le carburant de vos tests. Une mauvaise gestion entraîne : duplication, instabilité, dépendance à l’environnement. En Clean QA, on applique trois règles :

  • Centralisation : une seule source de vérité pour chaque type de données.
  • Abstraction : ne pas exposer directement les formats (CSV, SQL), mais passer par des Builders ou Factories.
  • Flexibilité : générer dynamiquement quand c’est pertinent (utilisateur aléatoire, timestamp).

Exemple en Java

Product product = ProductBuilder.aProduct()
                    .withName("Laptop Gamer")
                    .withPrice(1499.90)
                    .withStock(50)
                    .build();
    

Résultat : plus de duplication. Chaque dataset est créé de manière contrôlée, et la maintenance devient triviale.

4. CI/CD et exécution massive

Les bonnes pratiques doivent survivre au passage à l’échelle. En CI/CD, les tests deviennent massifs, parallélisés, exécutés plusieurs fois par jour. Quelques règles :

  • Indépendance : chaque test doit pouvoir tourner isolément.
  • Parallélisation : éviter les dépendances globales (BDD partagée sans reset).
  • Observabilité : logs, screenshots et métriques automatiques en cas d’échec.
  • Idempotence : un test doit produire le même résultat, quelle que soit l’exécution.
Exemple : un test qui supprime un utilisateur doit créer son utilisateur en setup, et non pas supposer son existence.

5. Revues de code QA

Les tests sont du code, et doivent donc être soumis à review. Une bonne pratique Clean QA impose :

  • Analyse des noms (clarté métier).
  • Vérification de l’absence de duplication.
  • Validation des assertions.
  • Respect des patterns (Page Object, Screenplay, etc.).

6. Logs et reporting

Les logs QA doivent être orientés diagnostic. Pas de “Test échoué” générique. Un bon log dit quoi, et pourquoi.

// Mauvais
FAIL

// Bon
[CheckoutSteps] Échec : ajout d’article impossible
Cause : stock insuffisant
  

7. Anti-patterns QA

Les mauvaises pratiques sont des bombes à retardement. En voici les plus courantes :

  • Steps silencieux : clics sans vérification ➝ illusion de sécurité.
  • Données en dur : duplication, instabilité.
  • Dépendances cachées : test B ne marche que si test A a été exécuté.
  • Assertions génériques : incapables de détecter une vraie régression.
  • Ignorer un flaky test : “on le relancera” ➝ perte totale de confiance.
Mauvaise pratique Conséquence Bonne pratique associée
Données en dur Tests cassent à chaque changement Centralisation via Builders/Factories
Assertions génériques Faux positifs Assertions explicites et contextuelles
Steps silencieux Test non valide Au moins une assertion par scénario

Cas pratique : refactorisation

Exemple d’un test instable avant/après refactorisation :

// Mauvais
click("#login");
type("#user","alice");
type("#pwd","secret");
click("#ok");
assertTrue(page.contains("OK"));

// Bon
loginSteps.seConnecter("alice","secret");
assertTrue(dashboard.isVisible(),
          "L’utilisateur doit accéder au tableau de bord après login.");
  
Résultat : plus lisible, plus fiable, plus proche du langage métier.

Quiz de validation

⚠️ Essayez de répondre par vous-même avant de regarder la solution. (Ne trichez pas ! Cliquez seulement si vous avez terminé 😉)

  1. Pourquoi un “step silencieux” est-il dangereux ?
  2. Citez 3 manières de centraliser vos datasets.
  3. Que doit contenir un log QA pour être utile ?
  4. Comment garantir l’indépendance des tests en CI/CD ?
  5. Donnez un exemple d’anti-pattern et sa contre-mesure.
Voir les réponses
  1. Un step silencieux est dangereux car il exécute une action sans vérifier le résultat ➝ faux sentiment de sécurité, incapacité à détecter une régression.
  2. Trois manières de centraliser les datasets :
    • Utiliser des Builders (flexibles, paramétrables).
    • Passer par des Factories (valeurs par défaut).
    • Brancher une source externe dynamique (BDD, API, CSV).
  3. Un log QA utile doit contenir : le contexte (où), l’action (quoi), le résultat attendu, et la cause si échec. Exemple : “Échec : ajout d’article impossible – stock insuffisant”.
  4. L’indépendance des tests en CI/CD se garantit par :
    • Création des données en setup à chaque test,
    • Isolation des environnements (containers éphémères),
    • Pas de dépendance entre tests.
  5. Exemple d’anti-pattern : utiliser des données en dur. Contre-mesure : centraliser via Builder/Factory pour éviter duplication et instabilité.

Exercice pratique

Refactorisez ce test spaghetti en Clean QA :

// Mauvais
driver.findElement(By.id("search")).sendKeys("Laptop");
driver.findElement(By.id("add")).click();
driver.findElement(By.id("checkout")).click();
  

Objectif : transformer en Steps métier + datasets centralisés + assertions explicites.

Checklist finale

  • ✔️ Chaque test a une assertion claire.
  • ✔️ Aucun dataset en dur.
  • ✔️ Steps = métier, Core = technique.
  • ✔️ Tests indépendants et parallélisables.
  • ✔️ Logs orientés diagnostic.
Si plus de deux cases ne sont pas cochées, votre framework n’est pas Clean QA : corrigez avant d’ajouter de nouveaux tests.

En résumé

Les bonnes pratiques ne sont pas une option : ce sont des garanties de survie. Les ignorer, c’est condamner vos tests à devenir un poids mort. Les appliquer, c’est transformer la QA en un levier stratégique de confiance et de performance.

Cas 1 : E-commerce – du spaghetti au Clean QA

Ce cas illustre le passage d’un test UI fragile et illisible à une implémentation Clean QA : architecture modulaire, données centralisées, assertions explicites, observabilité et intégration CI/CD. L’objectif est de rendre les tests lisibles, robustes, évolutifs… et crédibles pour les métiers.

1) Contexte & objectifs

Scénario métier : un client se connecte, recherche un produit, l’ajoute au panier et paie par carte. Aujourd’hui, le test échoue aléatoirement (UI lente, sélecteurs fragiles), les données sont dupliquées, les logs sont pauvres et l’analyse coûte du temps à l’équipe.

  • Objectif fonctionnel : garantir qu’un achat standard fonctionne bout-à-bout.
  • Objectif technique : éliminer la dette QA : duplications, flaky, illisibilité.

2) Version spaghetti (classique et fragile)

Extrait de test “tout-en-un”

// Exemple fragile
driver.findElement(By.id("username")).sendKeys("alice");
driver.findElement(By.id("password")).sendKeys("secret123");
driver.findElement(By.id("loginButton")).click();

driver.findElement(By.id("searchBox")).sendKeys("Laptop Gamer");
driver.findElement(By.id("searchButton")).click();
driver.findElement(By.xpath("//div[@class='product'][1]/button")).click();

driver.findElement(By.id("checkout")).click();
driver.findElement(By.id("payByCard")).click();
assertTrue(driver.getPageSource().contains("Merci"));
    
  • Mix métier/technique : le test manipule directement les locators.
  • Données en dur : “alice”, “Laptop Gamer”, XPath fragile indexé.
  • Assertion générique : “Merci” dans le HTML ➝ faux positifs possibles.
  • Pas d’observabilité : logs inexistants, pas de screenshots en cas d’échec.
Risque : chaque changement UI casse des dizaines de tests. Le flaky s’installe, la confiance s’effondre.

3) Plan de refactorisation (approche Clean QA)

  1. Isoler les interactions dans un Core (opérations génériques + waits robustes).
  2. Introduire des Steps métier (login, recherche, panier, checkout) — zéro locator dedans.
  3. Centraliser les données via Builders/Factories (User, Product, Payment).
  4. Ports & Adapters pour le paiement (UI vs API), activable par configuration.
  5. Assertions explicites (ex. orderPage.messageConfirmationAffiche()).
  6. Observateurs (screenshots, HAR/logs) branchés automatiquement en cas d’échec.
  7. Intégration CI/CD : parallélisation, artefacts, tags, matrice d’environnements.

4) Implémentation Clean QA — exemples de code

4.1 Core : opérations d’écran + “wait strategy” fiable

// Core abstrait (ex. Java)
public class Screen {
  private final WebDriver driver;
  public Screen(WebDriver driver) { this.driver = driver; }

  public void click(By by) { waitVisible(by).click(); }
  public void type(By by, String txt) {
    WebElement e = waitVisible(by);
    e.clear(); e.sendKeys(txt);
  }
  public boolean isVisible(By by) {
    try { return new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(ExpectedConditions.visibilityOfElementLocated(by)) != null;
    } catch (TimeoutException te) { return false; }
  }
  private WebElement waitVisible(By by) {
    return new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(ExpectedConditions.visibilityOfElementLocated(by));
  }
}
    

4.2 Page Objects : locators centralisés et stables

// Locators stables (data-testid recommandés)
public class LoginPage {
  public static final By USER = By.cssSelector("[data-testid='login-user']");
  public static final By PWD  = By.cssSelector("[data-testid='login-pwd']");
  public static final By BTN  = By.cssSelector("[data-testid='login-submit']");
}
public class SearchPage {
  public static final By BOX = By.cssSelector("[data-testid='search-box']");
  public static final By BTN = By.cssSelector("[data-testid='search-submit']");
  public static final By FIRST_ADD =
      By.cssSelector("[data-testid='product']:nth-of-type(1) [data-testid='add-to-cart']");
}
public class OrderPage {
  public static final By CONFIRM_MSG = By.cssSelector("[data-testid='order-confirmation']");
}
    

4.3 Steps métier : lisibles, zéro locator

public class LoginSteps {
  private final Screen screen;
  public LoginSteps(Screen s){ this.screen=s; }
  public void seConnecter(User u){
    screen.type(LoginPage.USER, u.getName());
    screen.type(LoginPage.PWD, u.getPassword());
    screen.click(LoginPage.BTN);
  }
}

public class SearchSteps {
  private final Screen screen;
  public SearchSteps(Screen s){ this.screen=s; }
  public void rechercherProduit(Product p){
    screen.type(SearchPage.BOX, p.getName());
    screen.click(SearchPage.BTN);
  }
  public void ajouterPremierResultat(){
    screen.click(SearchPage.FIRST_ADD);
  }
}
public class OrderSteps {
  private final Screen screen;
  public OrderSteps(Screen s){ this.screen=s; }
  public boolean confirmationAffichee(){
    return screen.isVisible(OrderPage.CONFIRM_MSG);
  }
}
    

4.4 Builders & Factories : données de test centralisées

public class User {
  private String name, password, role;
  // getters/setters...
}

public class UserBuilder {
  private String name = "alice";
  private String password = "secret123";
  private String role = "CLIENT";
  public static UserBuilder aUser(){ return new UserBuilder(); }
  public UserBuilder withName(String n){ this.name=n; return this; }
  public UserBuilder withPassword(String p){ this.password=p; return this; }
  public UserBuilder withRole(String r){ this.role=r; return this; }
  public User build(){ User u=new User(); u.setName(name); u.setPassword(password); u.setRole(role); return u; }
}

public class Product { private String name; /* ... */ }

public class ProductBuilder {
  private String name = "Laptop Gamer";
  public static ProductBuilder aProduct(){ return new ProductBuilder(); }
  public ProductBuilder withName(String n){ this.name=n; return this; }
  public Product build(){ Product p=new Product(); p.setName(name); return p; }
}
    

4.5 Ports & Adapters : paiement UI ou API au choix

public interface PaymentPort { boolean process(User user, Order order); }

public class UiPaymentAdapter implements PaymentPort {
  private final Screen screen;
  public UiPaymentAdapter(Screen s){ this.screen=s; }
  public boolean process(User user, Order order){
    screen.click(By.cssSelector("[data-testid='pay-card']"));
    return true; // + assertions UI ciblées
  }
}

public class ApiPaymentAdapter implements PaymentPort {
  private final PaymentApiClient api;
  public ApiPaymentAdapter(PaymentApiClient a){ this.api=a; }
  public boolean process(User user, Order order){
    return api.charge(order.getId(), user.getName());
  }
}
    

4.6 Scénario final lisible (composition)

// Wiring
Screen screen = new Screen(driver);
LoginSteps login = new LoginSteps(screen);
SearchSteps search = new SearchSteps(screen);
OrderSteps order = new OrderSteps(screen);

// Data
User user = UserBuilder.aUser().withName("alice").withPassword("secret123").build();
Product product = ProductBuilder.aProduct().withName("Laptop Gamer").build();

// Flow
login.seConnecter(user);
search.rechercherProduit(product);
search.ajouterPremierResultat();

// Paiement via port (configurable)
PaymentPort port = useApi ? new ApiPaymentAdapter(new PaymentApiClient())
                          : new UiPaymentAdapter(screen);
boolean ok = port.process(user, new Order(/*...*/));

// Assertion explicite
assertTrue(order.confirmationAffichee(),
  "Le message de confirmation de commande doit être visible.");
    

5) Données, idempotence & nettoyage

  • Stratégies : Builders/Factories, seeds stables, horodatage pour unicité (emails, n° commande).
  • Idempotence : chaque test prépare ses données en setup, ne dépend d’aucun autre test.
  • Nettoyage : best-effort cleanup ou environnements éphémères (containers DB jetables).
Astuce : préférez des data-testid à des XPaths indexés. Demandez aux devs d’ajouter ces hooks de test : énorme gain de robustesse.

6) Réduction du flaky : techniques efficaces

  • Waits déterministes (attendre la condition utile, pas un sleep arbitraire).
  • Locators stables (data-testid) au lieu de XPath fragiles.
  • Rejouer nativement des opérations réseau instables (API client avec retry/backoff).
  • Timeouts homogènes (Core), pas éparpillés dans les tests.
  • Isolation des effets de bord (données uniques, pas d’état partagé).
Re-runner “jusqu’à ce que ça passe” n’est pas une stratégie de qualité. Corrigez les causes racines (locators, waits, données, idempotence).

7) CI/CD, parallélisation & observabilité

  • Tags (ex. @smoke, @e2e, @slow) pour filtrer les suites par pipeline.
  • Parallélisation par files ou par tags, environnements isolés.
  • Artefacts : logs, screenshots, vidéos/HAR attachés au rapport (Allure/Xray/TestOps).
  • Variables CI pour choisir l’adapter (UI vs API) sans changer le code.

Exemple (pseudo YAML CI)

strategy:
  matrix:
    suite: [smoke, e2e]
    channel: [ui, api]
steps:
  - run: mvn test -Dgroups=${{ matrix.suite }} -Dpayment.channel=${{ matrix.channel }}
  - publish: allure-results/
    

8) KPI : mesurer l’amélioration

KPIAvantAprès Clean QA
Taux de flaky12–18%< 2%
Temps debug moyen45 min10–15 min (logs/screenshots)
Tests cassés lors d’un changement UI~60< 5 (locators centralisés)
Temps d’exécution pipeline35 min18 min (parallélisation)

9) Catalogue “odeurs ➝ correctifs Clean QA”

Odeur (smell)ImpactCorrectif Clean QA
XPath indexéFragile, casse souventdata-testid + Page Object
Données en durDuplication, instableBuilders/Factories + seeds
sleep(…)Flaky, lenteurWaits conditionnels dans le Core
Assertions vaguesFaux positifsAssertions explicites (pages/steps)
Couplage à l’outilRework massifPorts & Adapters (UI/API)
Logs pauvresDebug lentObserver : logs/screenshots/vidéos

10) Exercice – refactoriser une recherche/tri

But : transformer un test spaghetti de recherche + tri en version Clean QA.

Spaghetti à corriger

driver.findElement(By.id("searchBox")).sendKeys("Phone");
driver.findElement(By.id("searchButton")).click();
driver.findElement(By.id("sortPrice")).click();
assertTrue(driver.getPageSource().contains("OK"));
    
Voir une proposition de correction (spoiler)
// Data
Product product = ProductBuilder.aProduct().withName("Phone").build();

// Steps
searchSteps.rechercherProduit(product);
searchSteps.trierParPrixAscendant();

// Assertion explicite
assertTrue(searchResults.premierResultatCorrespond(product),
  "Le premier résultat doit correspondre au produit recherché après tri.");
    
Ajoutez les locators côté SearchPage et les méthodes trierParPrixAscendant(), premierResultatCorrespond(...) dans vos Pages/Steps.

11) Quiz – valider la compréhension

Essayez de répondre avant d’ouvrir les réponses 👀

  1. Pourquoi les data-testid sont-ils préférables aux XPaths indexés ?
  2. En quoi Ports & Adapters aide pour les paiements (UI vs API) ?
  3. Donnez 3 leviers concrets pour réduire le flaky.
  4. Quelle différence entre assertion générique et assertion explicite ?
  5. Comment rendez-vous vos tests idempotents en CI ?
Voir les réponses
  1. IDs stables dédiés aux tests ➝ moins sensibles aux changements de structure/présentation.
  2. On change d’adapter par config sans réécrire les scénarios (même use-case, canal différent).
  3. Waits conditionnels, locators stables, données uniques/idempotentes, retry réseau ciblé, isolation d’environnement.
  4. L’explicite vérifie un état métier précis (ex. message de confirmation visible), pas un simple mot-clé vague.
  5. Setup autonome, génération de données uniques, pas de dépendance inter-tests, cleanup/environnements éphémères.

12) Checklist d’adoption (OK/KO)

  • ✔️ Locators centralisés (Page Objects) et stables (data-testid).
  • ✔️ Steps 100% métier (aucun locator directement dans les tests).
  • ✔️ Données via Builders/Factories (aucune valeur critique “en dur”).
  • ✔️ Assertions explicites, orientées métier.
  • ✔️ Observabilité branchée (logs, screenshots, artefacts CI).
  • ✔️ Exécution parallélisable, tests idempotents.
  • ✔️ Paiement via Port, adapter UI/API interchangeable par config.
Si tout est coché : vous avez un cas e-commerce Clean QA fiable, lisible et industrialisable 🚀

Cas 2 : Application mobile – fidélité, QR code & offline

Ce cas illustre les défis spécifiques du test mobile (Android/iOS) : gestion de l'offline, interactions tactiles, locators fragiles, dépendances réseau. L'objectif est de passer d'une suite Appium chaotique à une architecture Clean QA mobile-first.

1) Contexte & objectifs

Scénario métier : Un client ouvre l'app, scanne un QR code en magasin, cumule des points de fidélité, puis les consulte hors ligne. Aujourd'hui, les tests échouent régulièrement (XPath complexes, timeouts réseau, gestion offline absente).

  • Objectif fonctionnel : garantir un parcours fidélité fluide online/offline.
  • Objectif technique : éliminer les XPath fragiles, gérer le mode offline, mocker le réseau.

2) Version spaghetti (Appium chaotique)

Extrait de test mobile fragile

// Exemple Appium fragile
driver.findElement(By.xpath("//android.widget.Button[@text='Scanner']")).click();
Thread.sleep(3000); // Attente camera
driver.findElement(By.xpath("//android.widget.EditText[1]")).sendKeys("QR123456");
driver.findElement(By.xpath("//android.widget.Button[contains(@text,'Valider')]")).click();

// Vérification points (assertion vague)
String source = driver.getPageSource();
assertTrue(source.contains("points"));
    
  • XPath indexés : `//android.widget.EditText[1]` ➝ casse si l'UI change.
  • Thread.sleep() : temps d'attente arbitraire, source de flaky.
  • Pas de gestion offline : teste uniquement le mode connecté.
  • Dépendance réseau : échoue si l'API backend est lente.
  • Assertions vagues : "points" pourrait être n'importe où dans la page.
Risque : Sur mobile, chaque update de l'UI casse des dizaines de tests. Le flaky explose à cause des délais réseau et des animations.

3) Plan de refactorisation (approche Clean QA mobile)

  1. Locators stables : utiliser les accessibility IDs / resource-ids plutôt que XPath.
  2. Page Objects mobile : encapsuler les interactions par écran.
  3. Waits conditionnels : remplacer sleep() par des attentes explicites.
  4. Mock réseau : simuler les réponses API pour tester offline.
  5. Steps métier : scannerQRCode(), consulterPoints(), passerEnModeOffline().
  6. Gestion des permissions : caméra, localisation via helpers dédiés.
  7. Tests multi-devices : matrice Android/iOS avec Device Farm ou cloud.

4) Implémentation Clean QA mobile

4.1 Locators stables (accessibility ID / resource-id)

// Avant (fragile)
By.xpath("//android.widget.Button[@text='Scanner']")

// Après (stable)
// Android
By.id("com.app:id/btn_scan")
// iOS
MobileBy.AccessibilityId("btn_scan")
    

4.2 Page Objects mobile

public class FidelityScreen {
  private final AppiumDriver driver;
  
  // Locators
  private final By BTN_SCAN = MobileBy.id("btn_scan");
  private final By INPUT_QR = MobileBy.id("input_qr_code");
  private final By BTN_VALIDATE = MobileBy.id("btn_validate");
  private final By TXT_POINTS = MobileBy.id("txt_points_balance");
  
  public FidelityScreen(AppiumDriver d) { this.driver = d; }
  
  public void scannerQRCode(String code) {
    waitAndClick(BTN_SCAN);
    waitAndType(INPUT_QR, code);
    waitAndClick(BTN_VALIDATE);
  }
  
  public String obtenirSoldePoints() {
    return waitForElement(TXT_POINTS).getText();
  }
  
  // Waits robustes
  private void waitAndClick(By by) {
    new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(ExpectedConditions.elementToBeClickable(by))
      .click();
  }
  
  private void waitAndType(By by, String text) {
    WebElement el = new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(ExpectedConditions.visibilityOfElementLocated(by));
    el.clear();
    el.sendKeys(text);
  }
  
  private WebElement waitForElement(By by) {
    return new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(ExpectedConditions.visibilityOfElementLocated(by));
  }
}
    

4.3 Steps métier mobile

public class FidelitySteps {
  private final FidelityScreen screen;
  private final NetworkHelper network;
  
  public FidelitySteps(AppiumDriver driver) {
    this.screen = new FidelityScreen(driver);
    this.network = new NetworkHelper(driver);
  }
  
  public void scannerCodeFidelite(String qrCode) {
    screen.scannerQRCode(qrCode);
  }
  
  public int consulterPoints() {
    String points = screen.obtenirSoldePoints();
    return Integer.parseInt(points.replaceAll("[^0-9]", ""));
  }
  
  public void passerEnModeOffline() {
    network.disableNetwork();
  }
  
  public void retourEnModeOnline() {
    network.enableNetwork();
  }
}
    

4.4 Mock réseau & mode offline

public class NetworkHelper {
  private final AppiumDriver driver;
  
  public NetworkHelper(AppiumDriver d) { this.driver = d; }
  
  // Android
  public void disableNetwork() {
    if (driver instanceof AndroidDriver) {
      ((AndroidDriver) driver).setConnection(new ConnectionStateBuilder()
        .withWiFiDisabled()
        .withDataDisabled()
        .build());
    }
  }
  
  public void enableNetwork() {
    if (driver instanceof AndroidDriver) {
      ((AndroidDriver) driver).setConnection(new ConnectionStateBuilder()
        .withWiFiEnabled()
        .withDataEnabled()
        .build());
    }
  }
  
  // Alternative : mock via proxy (BrowserMob, Charles)
  public void mockApiResponse(String endpoint, String jsonResponse) {
    // Configuration du proxy pour intercepter les calls API
    // et retourner des réponses mockées
  }
}
    

4.5 Scénario final (online + offline)

// Setup
AppiumDriver driver = new AndroidDriver(url, capabilities);
FidelitySteps fidelity = new FidelitySteps(driver);

// Test online
fidelity.scannerCodeFidelite("QR123456");
int pointsAvant = fidelity.consulterPoints();
assertEquals(100, pointsAvant, "Solde initial doit être 100 points");

// Passage offline
fidelity.passerEnModeOffline();

// Vérifier que les données sont en cache
int pointsOffline = fidelity.consulterPoints();
assertEquals(100, pointsOffline, "Les points doivent être visibles offline");

// Retour online
fidelity.retourEnModeOnline();

// Nouveau scan
fidelity.scannerCodeFidelite("QR789012");
int pointsApres = fidelity.consulterPoints();
assertTrue(pointsApres > pointsAvant, "Le solde doit avoir augmenté");
    

5) Gestion des permissions (caméra, localisation)

Helper permissions

public class PermissionHelper {
  private final AppiumDriver driver;
  
  public void accepterPermissionCamera() {
    if (driver instanceof AndroidDriver) {
      try {
        driver.findElement(By.id("com.android.packageinstaller:id/permission_allow_button"))
          .click();
      } catch (NoSuchElementException e) {
        // Permission déjà accordée
      }
    } else if (driver instanceof IOSDriver) {
      driver.findElement(By.xpath("//XCUIElementTypeButton[@name='OK']"))
        .click();
    }
  }
}
    

6) Données de test mobile

  • QR codes de test : dataset centralisé avec codes valides/invalides/expirés.
  • Comptes utilisateurs : Builders avec états de fidélité variés (nouveau client, VIP, etc.).
  • Réinitialisation : cleanup de l'app entre tests (clear app data ou réinstallation).

Builder mobile

public class QRCodeBuilder {
  private String code = "QR" + UUID.randomUUID().toString().substring(0, 8);
  private int points = 50;
  private boolean expired = false;
  
  public static QRCodeBuilder aQRCode() { return new QRCodeBuilder(); }
  
  public QRCodeBuilder withCode(String c) { this.code = c; return this; }
  public QRCodeBuilder withPoints(int p) { this.points = p; return this; }
  public QRCodeBuilder expired() { this.expired = true; return this; }
  
  public QRCode build() {
    QRCode qr = new QRCode();
    qr.setCode(code);
    qr.setPoints(points);
    qr.setExpired(expired);
    return qr;
  }
}
    

7) Réduction du flaky mobile

  • Waits intelligents : attendre la visibilité/clickabilité, pas des délais fixes.
  • Locators stables : accessibility ID > resource-id > XPath.
  • Gestion des animations : désactiver les animations système sur les devices de test.
  • Retry ciblé : retry sur les interactions sensibles (tap, swipe) avec backoff.
  • Clean state : réinstaller l'app ou clear data entre tests pour isolation.

Désactiver animations Android (ADB)

# Via ADB
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0

# Ou via capabilities Appium
capabilities.setCapability("disableWindowAnimation", true);
    

8) CI/CD & parallélisation mobile

  • Emulateurs/Simulateurs : Android Studio AVD, Xcode Simulator en CI.
  • Device farms : AWS Device Farm, BrowserStack, Sauce Labs pour vrais devices.
  • Parallélisation : plusieurs devices en parallèle (matrice OS/versions).
  • Artefacts : screenshots, logs Appium, enregistrements vidéo des tests.

Matrice CI mobile (GitHub Actions)

strategy:
  matrix:
    platform: [android, ios]
    version: [12, 13, 14]
steps:
  - name: Run mobile tests
    run: |
      mvn test \
        -Dplatform=${{ matrix.platform }} \
        -DplatformVersion=${{ matrix.version }}
  - name: Upload artifacts
    uses: actions/upload-artifact@v3
    with:
      name: test-results-${{ matrix.platform }}-${{ matrix.version }}
      path: target/screenshots/
    

9) KPI mobile

MétriqueAvantAprès Clean QA
Taux de flaky25-30%< 5%
Temps d'exécution45 min (séquentiel)12 min (parallèle)
Tests cassés par update UI~40< 3
Couverture offline0%80%

10) Odeurs mobile ➝ Correctifs

Odeur mobileImpactCorrectif Clean QA
XPath indexéCasse à chaque updateAccessibility ID / resource-id
Thread.sleep()Flaky, lenteurWebDriverWait conditionnels
Pas de test offlineBugs non détectésNetworkHelper + mock réseau
Permissions non géréesTests bloquésPermissionHelper dédié
Animations non désactivéesTiming instableADB settings / capabilities

11) Exercice – refactoriser un swipe

Transformer un test de swipe fragile en version Clean QA.

Spaghetti à corriger

// Swipe fragile
TouchAction swipe = new TouchAction(driver);
swipe.press(PointOption.point(500, 1000))
  .waitAction(WaitOptions.waitOptions(Duration.ofMillis(500)))
  .moveTo(PointOption.point(500, 300))
  .release()
  .perform();
    
Voir la correction
// SwipeHelper centralisé
public class SwipeHelper {
  private final AppiumDriver driver;
  
  public void swipeUp() {
    Dimension size = driver.manage().window().getSize();
    int startX = size.width / 2;
    int startY = (int) (size.height * 0.8);
    int endY = (int) (size.height * 0.2);
    
    new TouchAction(driver)
      .press(PointOption.point(startX, startY))
      .waitAction(WaitOptions.waitOptions(Duration.ofMillis(500)))
      .moveTo(PointOption.point(startX, endY))
      .release()
      .perform();
  }
}

// Utilisation
SwipeHelper swipe = new SwipeHelper(driver);
swipe.swipeUp();
    

12) Quiz mobile

  1. Pourquoi les accessibility IDs sont-ils préférables aux XPath sur mobile ?
  2. Comment tester un scénario offline sans désactiver le réseau physiquement ?
  3. Pourquoi désactiver les animations sur les devices de test ?
  4. Quelle différence entre émulateur et device farm cloud ?
Voir les réponses
  1. IDs stables, indépendants de la structure UI, plus rapides à localiser.
  2. Via NetworkHelper (setConnection) ou mock proxy pour simuler offline.
  3. Les animations créent des délais variables ➝ source de flaky.
  4. Émulateur = virtuel local (rapide, gratuit), Device Farm = vrais devices cloud (payant, realistic).

13) Checklist mobile Clean QA

  • ✔️ Locators via accessibility ID / resource-id (pas XPath indexés).
  • ✔️ Page Objects mobile avec waits conditionnels.
  • ✔️ Steps métier (scannerQRCode, consulterPoints, etc.).
  • ✔️ Gestion offline (NetworkHelper + tests dédiés).
  • ✔️ Permissions automatisées (PermissionHelper).
  • ✔️ Animations désactivées sur devices de test.
  • ✔️ CI/CD avec matrice Android/iOS.
  • ✔️ Artefacts (screenshots, vidéos, logs Appium).
Si tout est coché : vous avez un cas mobile Clean QA robuste et maintenable 📱

Cas 3 : LEGACY Reconnaissance visuelle

⚠️ Quand Selenium ne suffit pas
Pour certaines applications (terminaux de caisse, logiciels legacy Windows, interfaces VNC), il n'existe pas de DOM accessible. Selenium/WebDriver devient alors inutilisable. L'approche par reconnaissance visuelle (SikuliX + OCR) est la seule solution viable pour automatiser ces systèmes. Elle nécessite cependant une expertise spécifique pour éviter les pièges classiques (images dispersées, OCR instable, tests fragiles).

Ce cas illustre l'automatisation d'une caisse de magasin (Point of Sale). Contrairement au e-commerce web, le POS repose sur une interface graphique non-DOM (VNC, terminal physique, application Windows legacy). Les tests s'appuient sur SikuliX (vision par image) et l'OCR (EasyOCR via serveur Python HTTP) pour lire et valider les tickets. L'objectif : transformer des scripts fragiles en framework Clean QA robuste adapté aux contraintes "image only".

🎯 Besoin d'aide sur ce type d'automatisation ?
La mise en place d'un framework de tests visuels robuste demande une expertise pointue. Une formation d'initiation à cette approche est déjà disponible sur ce site pour découvrir les bases. Pour aller plus loin et former vos équipes ou mettre en place cette approche dans votre entreprise, n'hésitez pas à me contacter pour un accompagnement sur-mesure.

1) Contexte & objectifs

Scénario métier : Vente simple en caisse - scanner un article, l'ajouter au ticket, encaisser en espèces, vérifier que le ticket est correct (prix, total, TVA).

  • Objectif fonctionnel : garantir la fiabilité d'une transaction standard.
  • Objectif technique : éliminer les images dispersées, centraliser l'OCR, créer des Steps métier réutilisables.

2) Version spaghetti (SikuliX chaotique)

Script fragile typique

// Exemple POS fragile
screen.click("Images/bouton_article.png");
screen.type("Images/champ_ean.png", "1234567890123");
Thread.sleep(2000);
screen.click("Images/paiement_cash.png");

// OCR sans validation
String ocrResult = ocr.read("Images/zone_ticket.png");
assertTrue(ocrResult.contains("12,99"), "Prix trouvé dans le ticket");
    
  • Images dispersées : "Images/bouton_*.png" sans organisation.
  • Thread.sleep() : délais arbitraires source de flaky.
  • OCR non validé : pas de retry, pas de gestion d'erreur serveur.
  • Assertions vagues : "12,99" peut être n'importe où dans le ticket.
  • Pas de structure : logique métier mélangée avec technique.
Risque : Chaque changement d'UI casse des dizaines de tests. L'OCR échoue silencieusement, les tickets invalides passent inaperçus.

3) Plan de refactorisation (approche Clean QA POS)

  1. Core SikuliX : abstraction des opérations (click, type, waitFor).
  2. OCR centralisé : client HTTP pour serveur Python EasyOCR avec retry.
  3. Zones d'écran : définir ticketZone, buttonZone, clavier numpad.
  4. Images centralisées : classes ImageRepository par contexte.
  5. Steps métier : VenteSteps, EncaissementSteps, TicketSteps.
  6. Datasets POS : Articles, Remises, MoyensPaiement via Builders.
  7. Assertions OCR robustes : validation structurée des tickets.

4) Implémentation Clean QA POS

4.1 Core SikuliX : opérations robustes

public class ScreenCore {
  private final Screen screen;
  private static final int DEFAULT_TIMEOUT = 10;
  
  public ScreenCore() {
    this.screen = new Screen();
  }
  
  public void click(Pattern pattern) {
    waitFor(pattern);
    screen.click(pattern);
  }
  
  public void doubleClick(Pattern pattern) {
    waitFor(pattern);
    screen.doubleClick(pattern);
  }
  
  public void type(Pattern pattern, String text) {
    click(pattern);
    screen.paste(text); // Plus fiable que type() pour chiffres
  }
  
  public boolean waitFor(Pattern pattern) {
    return waitFor(pattern, DEFAULT_TIMEOUT);
  }
  
  public boolean waitFor(Pattern pattern, int seconds) {
    try {
      screen.wait(pattern, seconds);
      return true;
    } catch (FindFailed e) {
      return false;
    }
  }
  
  public Region defineRegion(int x, int y, int w, int h) {
    return new Region(x, y, w, h);
  }
}
    

4.2 OCR Client HTTP (avec routage multi-endpoints)

public class OCRClient {
  private static final String OCR_URL = "http://127.0.0.1:5000";
  private static final int MAX_RETRIES = 3;
  
  /**
   * Reconnaissance OCR avec routage selon type de caisse
   * @param imagePath chemin de l'image
   * @param caisseType type de caisse ("VARIANT_A", "VARIANT_B", null pour standard)
   */
  public static List<String> recognize(String imagePath, String caisseType) {
    // Routage endpoint selon type de caisse
    String endpoint = determineEndpoint(caisseType);
    
    for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
      try {
        String jsonResponse = performOCRRequest(imagePath, endpoint);
        List<String> texts = parseOCRResponse(jsonResponse);
        
        if (!texts.isEmpty()) {
          logInfo("OCR réussi via " + endpoint + " : " + texts.size() + " texte(s)");
          return texts;
        }
        
        if (attempt < MAX_RETRIES) {
          Thread.sleep(1000 * attempt); // Backoff progressif
        }
      } catch (Exception e) {
        if (attempt == MAX_RETRIES) {
          throw new RuntimeException("OCR failed after " + MAX_RETRIES + " attempts", e);
        }
        logWarning("Tentative " + attempt + " échouée : " + e.getMessage());
      }
    }
    return Collections.emptyList();
  }
  
  /**
   * Détermine l'endpoint OCR selon le type de caisse
   */
  private static String determineEndpoint(String caisseType) {
    if (caisseType == null || caisseType.isEmpty()) {
      return "/ocr"; // Endpoint standard
    }
    
    switch (caisseType.toUpperCase()) {
      case "VARIANT_A":
        return "/ocr_variant_a";
      case "VARIANT_B":
        return "/ocr_variant_b";
      default:
        return "/ocr";
    }
  }
  
  /**
   * Effectue la requête HTTP OCR
   */
  private static String performOCRRequest(String imagePath, String endpoint) throws Exception {
    HttpURLConnection conn = null;
    try {
      URL url = new URL(OCR_URL + endpoint);
      conn = (HttpURLConnection) url.openConnection();
      conn.setRequestMethod("POST");
      conn.setRequestProperty("Content-Type", "application/json");
      conn.setConnectTimeout(5000);
      conn.setReadTimeout(30000);
      conn.setDoOutput(true);
      
      // Payload JSON
      String json = "{\"image_path\":\"" + imagePath.replace("\\", "\\\\") + "\"}";
      
      try (OutputStream os = conn.getOutputStream()) {
        os.write(json.getBytes("UTF-8"));
      }
      
      // Lecture réponse
      int status = conn.getResponseCode();
      if (status != 200) {
        throw new Exception("HTTP " + status);
      }
      
      try (BufferedReader br = new BufferedReader(
          new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
        return br.lines().collect(Collectors.joining());
      }
      
    } finally {
      if (conn != null) conn.disconnect();
    }
  }
  
  /**
   * Parse la réponse JSON OCR
   */
  private static List<String> parseOCRResponse(String json) {
    List<String> texts = new ArrayList<>();
    // Parsing JSON pour extraire les textes détectés
    // ... (implémentation selon votre parser JSON)
    return texts;
  }
  
  /**
   * Trouve les coordonnées bbox d'un texte
   */
  public static int[] findTextCoordinates(String json, String searchText) {
    // Parse bbox pour retourner [x, y, width, height]
    // ... (votre implémentation existante)
    return null;
  }
}
    

4.3 Zones d'écran & Images centralisées

public class POSRegions {
  public static final Region TICKET_ZONE = new Region(50, 100, 400, 600);
  public static final Region BUTTON_ZONE = new Region(500, 100, 300, 500);
  public static final Region NUMPAD_ZONE = new Region(800, 300, 200, 300);
  public static final Region TOTAL_ZONE = new Region(50, 650, 400, 50);
}

public class POSImages {
  private static final String IMG_PATH = "Include/Images/POS/";
  
  // Boutons principaux
  public static final Pattern BTN_ARTICLE = new Pattern(IMG_PATH + "btn_article.png").similar(0.9f);
  public static final Pattern BTN_PAIEMENT = new Pattern(IMG_PATH + "btn_paiement.png").similar(0.9f);
  public static final Pattern BTN_ANNULER = new Pattern(IMG_PATH + "btn_annuler.png").similar(0.85f);
  
  // Paiement
  public static final Pattern PAY_CASH = new Pattern(IMG_PATH + "pay_cash.png").similar(0.9f);
  public static final Pattern PAY_CARD = new Pattern(IMG_PATH + "pay_card.png").similar(0.9f);
  
  // Champs de saisie
  public static final Pattern INPUT_EAN = new Pattern(IMG_PATH + "input_ean.png").similar(0.85f);
  public static final Pattern INPUT_QTY = new Pattern(IMG_PATH + "input_qty.png").similar(0.85f);
}
    

4.4 Steps métier POS

public class VenteSteps {
  private final ScreenCore screen;
  private final OCRClient ocr;
  
  public VenteSteps(ScreenCore s, OCRClient o) {
    this.screen = s;
    this.ocr = o;
  }
  
  public void scannerArticle(Article article) {
    screen.click(POSImages.BTN_ARTICLE);
    screen.type(POSImages.INPUT_EAN, article.getEan());
    screen.pressEnter();
  }
  
  public void ajouterQuantite(Article article, int qty) {
    scannerArticle(article);
    screen.click(POSImages.INPUT_QTY);
    screen.type(POSImages.INPUT_QTY, String.valueOf(qty));
    screen.pressEnter();
  }
}

public class EncaissementSteps {
  private final ScreenCore screen;
  
  public void payerEnEspeces(double montant) {
    screen.click(POSImages.BTN_PAIEMENT);
    screen.click(POSImages.PAY_CASH);
    saisirMontant(montant);
    screen.pressEnter();
  }
  
  public void payerParCarte() {
    screen.click(POSImages.BTN_PAIEMENT);
    screen.click(POSImages.PAY_CARD);
    screen.waitFor(POSImages.PAYMENT_OK, 30);
  }
  
  private void saisirMontant(double montant) {
    String amount = String.format("%.2f", montant).replace(".", "");
    screen.type(POSImages.NUMPAD_ZONE, amount);
  }
}

public class TicketSteps {
  private final OCRClient ocr;
  private final String caisseType;
  
  public TicketSteps(OCRClient o, String type) {
    this.ocr = o;
    this.caisseType = type;
  }
  
  public boolean totalEstCorrect(double expectedTotal) {
    String screenshot = captureTicketZone();
    List<String> texts = ocr.recognize(screenshot, caisseType);
    
    String totalLine = texts.stream()
      .filter(t -> t.toLowerCase().contains("total"))
      .findFirst()
      .orElse("");
    
    return totalLine.contains(String.format("%.2f", expectedTotal));
  }
  
  public boolean articlePresent(Article article) {
    String screenshot = captureTicketZone();
    List<String> texts = ocr.recognize(screenshot, caisseType);
    
    return texts.stream()
      .anyMatch(t -> t.contains(article.getLibelle()) || t.contains(article.getEan()));
  }
  
  private String captureTicketZone() {
    return POSRegions.TICKET_ZONE.screenshot();
  }
}
    

4.5 Builders & Datasets POS

public class Article {
  private String ean;
  private String libelle;
  private double prixHT;
  private double tauxTVA;
  // getters/setters
}

public class ArticleBuilder {
  private String ean = "1234567890123";
  private String libelle = "Article Test";
  private double prixHT = 10.00;
  private double tauxTVA = 20.0;
  
  public static ArticleBuilder unArticle() { return new ArticleBuilder(); }
  
  public ArticleBuilder withEan(String e) { this.ean = e; return this; }
  public ArticleBuilder withLibelle(String l) { this.libelle = l; return this; }
  public ArticleBuilder withPrixHT(double p) { this.prixHT = p; return this; }
  public ArticleBuilder withTVA(double t) { this.tauxTVA = t; return this; }
  
  public Article build() {
    Article a = new Article();
    a.setEan(ean);
    a.setLibelle(libelle);
    a.setPrixHT(prixHT);
    a.setTauxTVA(tauxTVA);
    return a;
  }
}

public class ArticlesCatalog {
  public static Article articleStandard() {
    return ArticleBuilder.unArticle()
      .withLibelle("Pain Tradition")
      .withEan("3250390000013")
      .withPrixHT(1.20)
      .withTVA(5.5)
      .build();
  }
  
  public static Article articlePromo() {
    return ArticleBuilder.unArticle()
      .withLibelle("Promo Week-end")
      .withEan("3250390999999")
      .withPrixHT(5.00)
      .withTVA(20.0)
      .build();
  }
}
    

4.6 Scénario final Clean QA

// Setup
ScreenCore screen = new ScreenCore();
OCRClient ocr = new OCRClient();

// Type de caisse (null, "VARIANT_A", "VARIANT_B", etc.)
String caisseType = GlobalVariable.caisse_type; 

VenteSteps vente = new VenteSteps(screen, ocr);
EncaissementSteps encaissement = new EncaissementSteps(screen);
TicketSteps ticket = new TicketSteps(ocr, caisseType);

// Data
Article article = ArticlesCatalog.articleStandard();

// Flow
vente.scannerArticle(article);
double total = article.getPrixTTC();

encaissement.payerEnEspeces(total);

// Assertions explicites
assertTrue(ticket.totalEstCorrect(total),
  "Le total du ticket doit correspondre au prix TTC de l'article");
assertTrue(ticket.articlePresent(article),
  "L'article doit être visible dans le ticket");
    

5) OCR & vision : techniques avancées

  • Routage multi-endpoints : support de plusieurs variants de caisses via endpoints dédiés (/ocr, /ocr_variant_a, etc.).
  • Retry automatique : 3 tentatives avec backoff progressif si OCR échoue.
  • Coordonnées bbox : récupération [x,y,w,h] pour clic précis sur texte détecté.
  • Zones prédéfinies : TICKET_ZONE, TOTAL_ZONE pour limiter le scope OCR.
  • Validation structurée : parsing JSON robuste avec gestion d'erreurs.
Astuce : Pour les tickets de caisse, capturez toujours la même zone (coordonnées fixes) pour avoir des résultats OCR cohérents entre exécutions.

6) Données POS & idempotence

  • Articles : EAN unique, libellés stables, prix/TVA centralisés.
  • Transactions : générer ID unique par test (timestamp + UUID).
  • Moyens paiement : enum CASH, CARD, VOUCHER avec stratégies dédiées.
  • Nettoyage : annuler transaction ou reset caisse entre tests.

7) Réduction du flaky POS

  • Similarity dynamique : 0.85 pour boutons génériques, 0.95 pour logos précis.
  • Waits conditionnels : waitFor(pattern) au lieu de sleep().
  • Zones stables : définir régions fixes pour éviter recherche plein écran.
  • OCR retry : 3 tentatives avec backoff si texte vide ou serveur timeout.
  • Isolation VM : Docker + VNC dédié par runner CI.

8) CI/CD POS

Pipeline avec Docker + VNC

# Docker avec affichage virtuel
services:
  pos-test:
    image: pos-automation:latest
    environment:
      - DISPLAY=:99
      - CAISSE_TYPE=VARIANT_A
    volumes:
      - ./screenshots:/screenshots
    command: |
      Xvfb :99 -screen 0 1920x1080x24 &
      python Include/scripts/python/ocr_server.py &
      mvn test -Dgroups=smoke
    
  • Artefacts : screenshots zones ticket, logs OCR JSON, vidéos VNC.
  • Parallélisation : plusieurs caisses virtuelles en parallèle.
  • Variables : CAISSE_TYPE injecté pour router vers le bon endpoint OCR.

9) KPI POS

KPIAvantAprès Clean QA
Taux de flaky18-25%< 3%
Temps debug moyen1h+15 min (screenshots + logs OCR)
Tests cassés par update UI~45< 6 (images centralisées)
Temps d'exécution38 min20 min (zones optimisées)

10) Odeurs POS ➝ Correctifs

OdeurImpactCorrectif Clean QA
Images disperséesDuplication, incohérencePOSImages centralisé
Thread.sleep()Flaky, lenteurwaitFor(pattern) conditionnel
OCR non validéFaux positifsRetry + parsing structuré
Zones aléatoiresOCR instableRégions fixes (TICKET_ZONE)
Endpoint uniqueCode dupliqué par variantRoutage multi-endpoints

11) Exercice – refactoriser une annulation

Spaghetti à corriger

screen.click("Images/article.png");
screen.type("Images/ean.png", "1234567890123");
Thread.sleep(2000);
screen.click("Images/annuler.png");
String ocr = ocr.read("ticket.png");
assertTrue(ocr.contains("annulé"));
    
Voir la correction
Article article = ArticleBuilder.unArticle()
  .withEan("1234567890123")
  .build();

vente.scannerArticle(article);
vente.annulerDerniereOperation();

assertTrue(ticket.articleAnnule(article),
  "L'article doit apparaître comme annulé dans le ticket");
    

12) Quiz POS

  1. Pourquoi utiliser des zones fixes (Region) plutôt que plein écran ?
  2. Quel est l'intérêt du routage multi-endpoints OCR ?
  3. Comment garantir l'idempotence des tests POS ?
  4. Pourquoi faire 3 retry sur l'OCR ?
Voir les réponses
  1. Performance (zone limitée) + stabilité (évite faux positifs hors contexte).
  2. Permet d'avoir un comportement OCR adapté selon le type de caisse (différents formats de tickets, langues, etc.).
  3. ID unique par transaction, reset caisse, pas de dépendance inter-tests.
  4. L'OCR peut échouer temporairement (serveur occupé, image floue), retry évite faux négatifs.

13) Checklist POS Clean QA

  • ✔️ Images centralisées (POSImages) avec similarity adaptée.
  • ✔️ Zones d'écran fixes (POSRegions) pour stabilité OCR.
  • ✔️ Client OCR avec retry + routage multi-endpoints.
  • ✔️ Steps métier (VenteSteps, EncaissementSteps, TicketSteps).
  • ✔️ Datasets via Builders (Articles, Remises, Paiements).
  • ✔️ Assertions OCR explicites (totalEstCorrect, articlePresent).
  • ✔️ Waits conditionnels (waitFor) au lieu de sleep.
  • ✔️ CI/CD avec Docker + VNC + artefacts (screenshots, logs OCR).
POS automatisé avec Clean QA : tests fiables même sans DOM, maintenables, industrialisables 🎯

Cas 4 : API / Microservices – du spaghetti au Clean QA

Ce cas illustre l'automatisation de tests d'APIs REST dans une architecture microservices. Le scénario couvre la création d'un client, l'ajout d'une commande, puis la validation du stock. Nous comparerons deux approches techniques (Postman/Newman et Java/RestAssured), chacune dans sa version spaghetti puis Clean QA.

📚 Formation disponible
Une formation approfondie sur les tests d'APIs et microservices est proposée sur ce site pour maîtriser les concepts avancés (contract testing, mocking, observabilité).

1) Contexte & objectifs

Scénario métier : Créer un client (POST /customers), ajouter une commande (POST /orders), vérifier que le stock est décrémenté (GET /products/{id}).

  • Objectif fonctionnel : garantir la cohérence inter-services (customer → order → stock).
  • Objectif technique : éliminer les payloads en dur, centraliser les validations JSON, gérer les environnements (dev/staging/prod).

2) Versions spaghetti

2.1 Postman spaghetti

// Collection "Tests API" - Request 1
POST {{baseUrl}}/customers
Body (raw JSON):
{
  "name": "Alice",
  "email": "alice@test.com",
  "address": "123 Main St"
}

Tests:
pm.test("Status 201", () => {
  pm.response.to.have.status(201);
});

// Request 2 (copié-collé)
POST {{baseUrl}}/orders
Body:
{
  "customerId": "12345", // ❌ En dur !
  "items": [{"productId": "SKU001", "quantity": 2}]
}

Tests:
pm.test("Order OK", () => {
  pm.expect(pm.response.json().total).to.be.above(0);
});
    

Problèmes :

  • ❌ IDs statiques ("12345") → échoue si client inexistant
  • ❌ Pas de chaînage entre requests
  • ❌ Assertions vagues (just "status 201")
  • ❌ Duplication des payloads

2.2 Java/RestAssured spaghetti

// Test fragmenté
@Test
public void testCreateCustomer() {
  String body = "{ \"name\": \"Alice\", \"email\": \"alice@test.com\" }";
  
  Response resp = RestAssured
    .given()
    .contentType("application/json")
    .body(body)
    .post("http://localhost:8080/customers");
  
  assertEquals(201, resp.getStatusCode());
  assertTrue(resp.asString().contains("Alice"));
}

@Test
public void testCreateOrder() {
  String body = "{ \"customerId\": \"12345\", \"items\": [...] }";
  
  Response resp = RestAssured
    .given()
    .body(body)
    .post("http://localhost:8080/orders");
  
  assertEquals(201, resp.getStatusCode());
}
    

Problèmes :

  • ❌ JSON en String brut (typos, pas de validation IDE)
  • ❌ URL hardcodée
  • ❌ Tests isolés sans dépendance (customerId perdu)
  • ❌ Assertions primitives (contains, equals)
Risque : Changement de contrat API = 50+ tests à mettre à jour manuellement. Pas de gestion d'idempotence, données polluées entre exécutions.

3) Plan de refactorisation

Objectifs communs aux deux approches :

  1. Payloads dynamiques : génération unique (UUID, timestamp)
  2. Chaînage de requêtes : passer les IDs entre étapes
  3. Assertions robustes : JSON Schema, headers, response time
  4. Environnements : dev/staging/prod configurables
  5. Cleanup : suppression des données de test

4) Implémentations Clean QA

4.1 Postman Clean QA

// Environment Variables
baseUrl: {{env.API_BASE_URL}}
customerId: (vide initialement)

// Pre-request Script (génération dynamique)
const uniqueEmail = `user_${Date.now()}@test.com`;
pm.environment.set("userEmail", uniqueEmail);

// Request 1: Create Customer
POST {{baseUrl}}/customers
Body:
{
  "name": "{{$randomFullName}}",
  "email": "{{userEmail}}",
  "address": "{{$randomStreetAddress}}"
}

Tests:
pm.test("Customer created", () => {
  pm.response.to.have.status(201);
  pm.response.to.have.jsonSchema(customerSchema);
  
  const json = pm.response.json();
  pm.expect(json.id).to.be.a('string');
  pm.expect(json.email).to.equal(pm.environment.get("userEmail"));
  
  // Chaînage : sauvegarder l'ID
  pm.environment.set("customerId", json.id);
});

// Request 2: Create Order (utilise customerId)
POST {{baseUrl}}/orders
Body:
{
  "customerId": "{{customerId}}",
  "items": [
    {
      "productId": "SKU001",
      "quantity": 2,
      "price": 29.99
    }
  ]
}

Tests:
pm.test("Order created for customer", () => {
  pm.response.to.have.status(201);
  
  const order = pm.response.json();
  pm.expect(order.customerId).to.equal(pm.environment.get("customerId"));
  pm.expect(order.total).to.equal(59.98);
  pm.expect(order.status).to.equal("PENDING");
  
  pm.environment.set("orderId", order.id);
});

// Cleanup (dernier test de la collection)
DELETE {{baseUrl}}/customers/{{customerId}}
    

4.2 Java/RestAssured Clean QA

// Builders
public class CustomerBuilder {
  private String name = "Default User";
  private String email = generateEmail();
  private String address = "123 Main St";
  
  public static CustomerBuilder aCustomer() { return new CustomerBuilder(); }
  
  public CustomerBuilder withName(String n) { this.name = n; return this; }
  public CustomerBuilder withEmail(String e) { this.email = e; return this; }
  
  public Customer build() {
    Customer c = new Customer();
    c.setName(name);
    c.setEmail(email);
    c.setAddress(address);
    return c;
  }
  
  private static String generateEmail() {
    return "user_" + System.currentTimeMillis() + "@test.com";
  }
}

// API Client
public class ApiClient {
  private final String baseUrl;
  
  public ApiClient(String url) {
    this.baseUrl = url;
    RestAssured.baseURI = url;
  }
  
  public Response post(String endpoint, Object body) {
    return given()
      .contentType("application/json")
      .body(body)
      .when().post(endpoint)
      .then().extract().response();
  }
  
  public Response get(String endpoint) {
    return given()
      .when().get(endpoint)
      .then().extract().response();
  }
}

// Steps
public class CustomerSteps {
  private final ApiClient api;
  
  public String creerClient(Customer customer) {
    Response resp = api.post("/customers", customer);
    
    resp.then()
      .statusCode(201)
      .body("id", notNullValue())
      .body("email", equalTo(customer.getEmail()));
    
    return resp.jsonPath().getString("id");
  }
}

public class OrderSteps {
  private final ApiClient api;
  
  public String creerCommande(String customerId, List items) {
    Order order = OrderBuilder.anOrder()
      .forCustomer(customerId)
      .withItems(items)
      .build();
    
    Response resp = api.post("/orders", order);
    
    resp.then()
      .statusCode(201)
      .body("customerId", equalTo(customerId))
      .body("total", greaterThan(0f))
      .body("status", equalTo("PENDING"));
    
    return resp.jsonPath().getString("id");
  }
}

// Test final
@Test
public void testCompletOrderFlow() {
  // Setup
  ApiClient api = new ApiClient(config.getBaseUrl());
  CustomerSteps customerSteps = new CustomerSteps(api);
  OrderSteps orderSteps = new OrderSteps(api);
  
  // Data
  Customer customer = CustomerBuilder.aCustomer()
    .withName("John Doe")
    .build();
  
  List items = Arrays.asList(
    new OrderItem("SKU001", 2, 29.99)
  );
  
  // Flow
  String customerId = customerSteps.creerClient(customer);
  String orderId = orderSteps.creerCommande(customerId, items);
  
  // Assertions finales
  assertNotNull(customerId);
  assertNotNull(orderId);
  
  // Cleanup
  api.delete("/customers/" + customerId);
}
    

5) Assertions contractuelles

JSON Schema validation (Postman)

// Définir le schema
const customerSchema = {
  "type": "object",
  "required": ["id", "name", "email"],
  "properties": {
    "id": { "type": "string", "pattern": "^[a-f0-9-]{36}$" },
    "name": { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" },
    "createdAt": { "type": "string", "format": "date-time" }
  }
};

// Utiliser dans les tests
pm.test("Schema validation", () => {
  pm.response.to.have.jsonSchema(customerSchema);
});

// Headers validation
pm.test("Headers check", () => {
  pm.response.to.have.header("Content-Type", "application/json");
  pm.response.to.have.header("X-Request-Id");
});

// Response time
pm.test("Response time", () => {
  pm.expect(pm.response.responseTime).to.be.below(2000);
});
    

JSON Schema validation (RestAssured)

import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

public class ContractAssertions {
  
  public static void assertCustomerContract(Response response) {
    response.then()
      .statusCode(201)
      .header("Content-Type", "application/json")
      .header("X-Request-Id", notNullValue())
      .body(matchesJsonSchemaInClasspath("schemas/customer-schema.json"))
      .time(lessThan(2000L));
  }
  
  public static void assertOrderContract(Response response, double expectedTotal) {
    response.then()
      .statusCode(201)
      .body(matchesJsonSchemaInClasspath("schemas/order-schema.json"))
      .body("total", equalTo((float) expectedTotal))
      .body("status", isIn(Arrays.asList("PENDING", "CONFIRMED")))
      .body("items", hasSize(greaterThan(0)));
  }
}
    

6) Builders payloads : données de test structurées

Pattern Builder avancé

public class OrderBuilder {
  private String customerId;
  private List items = new ArrayList<>();
  private String currency = "EUR";
  private String status = "PENDING";
  
  public static OrderBuilder anOrder() { return new OrderBuilder(); }
  
  public OrderBuilder forCustomer(String id) {
    this.customerId = id;
    return this;
  }
  
  public OrderBuilder withItem(String productId, int qty, double price) {
    items.add(new OrderItem(productId, qty, price));
    return this;
  }
  
  public OrderBuilder withRandomItems(int count) {
    for (int i = 0; i < count; i++) {
      withItem("SKU" + UUID.randomUUID().toString().substring(0, 8), 
               (int)(Math.random() * 5) + 1, 
               Math.random() * 100);
    }
    return this;
  }
  
  public OrderBuilder inCurrency(String curr) {
    this.currency = curr;
    return this;
  }
  
  public Order build() {
    Order o = new Order();
    o.setCustomerId(customerId);
    o.setItems(items);
    o.setCurrency(currency);
    o.setStatus(status);
    o.setTotal(calculateTotal());
    return o;
  }
  
  private double calculateTotal() {
    return items.stream()
      .mapToDouble(i -> i.getPrice() * i.getQty())
      .sum();
  }
}

// Factory pour cas courants
public class OrdersCatalog {
  public static Order simpleOrder(String customerId) {
    return OrderBuilder.anOrder()
      .forCustomer(customerId)
      .withItem("SKU001", 1, 29.99)
      .build();
  }
  
  public static Order bulkOrder(String customerId) {
    return OrderBuilder.anOrder()
      .forCustomer(customerId)
      .withRandomItems(10)
      .build();
  }
}
    

7) Réduction du flaky API

  • Retry HTTP : retry automatique sur timeout/503 (max 3 fois avec backoff)
  • Idempotence : UUID/timestamp pour garantir unicité des données
  • Isolation : cleanup systématique (DELETE après chaque test)
  • Timeouts cohérents : même valeur partout (pas 5s ici, 30s là)
  • Ordre des tests : pas de dépendance inter-tests (chacun autonome)
  • Stubs pour dépendances : WireMock pour services externes instables

Retry automatique (RestAssured)

public class ResilientApiClient extends ApiClient {
  private static final int MAX_RETRIES = 3;
  
  @Override
  public Response post(String endpoint, Object body) {
    Exception lastException = null;
    
    for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
      try {
        Response resp = super.post(endpoint, body);
        
        if (resp.getStatusCode() < 500) {
          return resp; // Success ou erreur client (4xx)
        }
        
        // 5xx = retry
        Thread.sleep(1000 * attempt); // Backoff
        
      } catch (Exception e) {
        lastException = e;
        if (attempt == MAX_RETRIES) break;
      }
    }
    
    throw new RuntimeException("API call failed after " + MAX_RETRIES + " attempts", lastException);
  }
}
    

8) CI/CD & exécution

Pipeline Postman (Newman)

# GitHub Actions
name: API Tests (Postman)

on: [push, pull_request]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Newman
        run: npm install -g newman newman-reporter-htmlextra
      
      - name: Run Postman Collection
        run: |
          newman run collections/api-tests.json \
            -e environments/${{ matrix.environment }}.json \
            --reporters cli,htmlextra \
            --reporter-htmlextra-export reports/newman-${{ matrix.environment }}.html
      
      - name: Upload Report
        uses: actions/upload-artifact@v3
        with:
          name: newman-report-${{ matrix.environment }}
          path: reports/
    

Pipeline Java (Maven)

# GitHub Actions
name: API Tests (RestAssured)

on: [push, pull_request]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
      
      - name: Run Tests
        run: |
          mvn test \
            -Denv=${{ matrix.environment }} \
            -Dgroups=api
      
      - name: Publish Test Report
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: API Tests (${{ matrix.environment }})
          path: target/surefire-reports/*.xml
          reporter: java-junit
    

9) KPI & résultats

KPIAvantAprès Clean QA
Taux de flaky15-20%< 2%
Temps debug moyen30 min5 min (logs structurés)
Tests cassés par changement contrat~50< 5 (schemas centralisés)
Temps d'exécution12 min4 min (parallélisation)
Couverture contrats20%95% (JSON Schema)

10) Odeurs API ➝ Correctifs

OdeurImpactCorrectif Clean QA
JSON en StringTypos, pas de validationPOJOs + Builders
IDs statiquesTests non idempotentsUUID/timestamp générés
Assertions vaguesFaux positifsJSON Schema + contrats
Pas de chaînageTests fragmentésVariables env / Steps
URLs hardcodéesPas multi-envConfig externalisée
Pas de retryFlaky réseauRetry automatique HTTP

11) Exercice – refactoriser un update

Spaghetti à corriger

// Postman
PUT {{baseUrl}}/customers/12345
Body:
{
  "name": "Bob Updated",
  "email": "bob@test.com"
}

Tests:
pm.test("Status 200", () => {
  pm.response.to.have.status(200);
});
    
Voir la correction
Postman Clean
// Pre-request: créer le client d'abord
const customerId = pm.environment.get("customerId");

PUT {{baseUrl}}/customers/{{customerId}}
Body:
{
  "name": "{{$randomFullName}}",
  "email": "{{userEmail}}"
}

Tests:
pm.test("Customer updated", () => {
  pm.response.to.have.status(200);
  pm.response.to.have.jsonSchema(customerSchema);
  
  const updated = pm.response.json();
  pm.expect(updated.id).to.equal(customerId);
  pm.expect(updated.email).to.equal(pm.environment.get("userEmail"));
});
    
Java Clean
// Setup
Customer original = CustomerBuilder.aCustomer().build();
String customerId = customerSteps.creerClient(original);

// Update
Customer updated = CustomerBuilder.aCustomer()
  .withName("Updated Name")
  .withEmail(original.getEmail()) // Garder même email
  .build();

Response resp = api.put("/customers/" + customerId, updated);

// Assertions
resp.then()
  .statusCode(200)
  .body("id", equalTo(customerId))
  .body("name", equalTo("Updated Name"));
    

12) Quiz API

  1. Pourquoi utiliser JSON Schema plutôt que des assertions simples ?
  2. Comment garantir l'idempotence des tests API ?
  3. Quelle différence entre Postman et RestAssured pour Clean QA ?
  4. Pourquoi faire du retry sur les appels HTTP ?
Voir les réponses
  1. Validation complète du contrat (structure + types + contraintes), détection précoce des breaking changes.
  2. UUID/timestamp uniques, cleanup systématique, pas de dépendance inter-tests, environnements isolés.
  3. Postman = rapide pour exploration/CI basique. RestAssured = framework robuste avec Builders/Steps/Ports.
  4. Résilience réseau (timeouts transitoires), services externes instables, évite faux négatifs.

13) Checklist API Clean QA

  • ✔️ Payloads générés dynamiquement (UUID/timestamp)
  • ✔️ Chaînage de requêtes (IDs passés entre étapes)
  • ✔️ Assertions contractuelles (JSON Schema + headers + timing)
  • ✔️ Steps métier (CustomerSteps, OrderSteps)
  • ✔️ Builders pour construire les payloads
  • ✔️ Gestion multi-environnements (dev/staging/prod)
  • ✔️ Cleanup automatique (DELETE après tests)
  • ✔️ Retry HTTP automatique (résilience)
  • ✔️ CI/CD avec Newman (Postman) ou Maven (Java)
  • ✔️ Logs structurés + correlation IDs
APIs testées avec Clean QA : contrats validés, tests idempotents, framework maintenable et scalable 🚀

🧩 Atelier pratique Clean QA

Cet atelier vous guide pas à pas dans la transformation d'un projet de test complet : de la dette technique à un framework Clean QA robuste. Vous allez refactoriser un scénario e-commerce end-to-end en appliquant tous les patterns vus dans la formation.

⏱️ Durée estimée : 3-4 heures
🎯 Objectif : Créer un framework testable, maintenable et industrialisable

Partie 1 : Le projet spaghetti 🍝

Contexte métier

Vous héritez d'un projet de test pour une boutique en ligne. Le scénario critique : un client se connecte, recherche un produit, l'ajoute au panier, applique un code promo, et paie par carte. Actuellement, les tests sont fragmentés, flaky (15% d'échecs aléatoires), et chaque changement UI casse une dizaine de tests.

Code actuel (extrait)

// Test fragmenté actuel
@Test
public void testAchatAvecPromo() {
  driver.get("https://shop.example.com");
  
  // Login en dur
  driver.findElement(By.id("username")).sendKeys("alice@test.com");
  driver.findElement(By.id("password")).sendKeys("password123");
  driver.findElement(By.id("loginBtn")).click();
  
  Thread.sleep(2000); // ⚠️ Flaky
  
  // Recherche avec XPath fragile
  driver.findElement(By.xpath("//input[@placeholder='Search']")).sendKeys("Laptop");
  driver.findElement(By.xpath("//button[text()='Search']")).click();
  
  // Ajout panier (indexé)
  driver.findElement(By.xpath("(//button[@class='add-to-cart'])[1]")).click();
  
  // Promo en dur
  driver.findElement(By.id("promoCode")).sendKeys("SUMMER2024");
  driver.findElement(By.id("applyPromo")).click();
  
  Thread.sleep(1000);
  
  // Paiement
  driver.findElement(By.id("checkout")).click();
  driver.findElement(By.id("payByCard")).click();
  
  // Assertion vague
  String pageSource = driver.getPageSource();
  assertTrue(pageSource.contains("Merci"));
}
    
Problèmes identifiés :
  • ❌ Credentials en dur (pas de gestion utilisateurs)
  • ❌ Thread.sleep() partout (source de flaky)
  • ❌ XPath indexés et fragiles
  • ❌ Données métier hardcodées (promo "SUMMER2024")
  • ❌ Assertions génériques (contains "Merci")
  • ❌ Pas de structure (tout dans un test)

Partie 2 : Votre mission 🎯

Refactorisez ce test en appliquant Clean QA :

  1. Créer un Core (Screen avec waits robustes)
  2. Extraire les Page Objects (locators centralisés)
  3. Définir les Steps métier (LoginSteps, SearchSteps, CheckoutSteps)
  4. Modéliser les données (User, Product, PromoCode via Builders)
  5. Implémenter Ports & Adapters (paiement UI vs API)
  6. Ajouter observabilité (screenshots, logs)
  7. Réécrire le test final (100% métier)

Partie 3 : Implémentation pas à pas 🛠️

Étape 1 : Le Core

📝 Instructions

Créez une classe Screen avec :

  • Méthode click(By locator) avec wait automatique
  • Méthode type(By locator, String text)
  • Méthode isVisible(By locator)
  • Wait strategy : WebDriverWait avec timeout 10s
✅ Solution
public class Screen {
  private final WebDriver driver;
  private static final int TIMEOUT = 10;
  
  public Screen(WebDriver driver) { 
    this.driver = driver; 
  }
  
  public void click(By by) {
    waitVisible(by).click();
  }
  
  public void type(By by, String text) {
    WebElement el = waitVisible(by);
    el.clear();
    el.sendKeys(text);
  }
  
  public boolean isVisible(By by) {
    try {
      waitVisible(by);
      return true;
    } catch (TimeoutException e) {
      return false;
    }
  }
  
  private WebElement waitVisible(By by) {
    return new WebDriverWait(driver, Duration.ofSeconds(TIMEOUT))
      .until(ExpectedConditions.visibilityOfElementLocated(by));
  }
}
    

Étape 2 : Page Objects

📝 Instructions

Créez les Page Objects suivants avec locators data-testid :

  • LoginPage : USER, PASSWORD, BTN_LOGIN
  • SearchPage : INPUT_SEARCH, BTN_SEARCH, BTN_ADD_TO_CART
  • CartPage : INPUT_PROMO, BTN_APPLY_PROMO, BTN_CHECKOUT
  • OrderPage : MSG_CONFIRMATION
✅ Solution
public class LoginPage {
  public static final By USER = By.cssSelector("[data-testid='login-email']");
  public static final By PASSWORD = By.cssSelector("[data-testid='login-password']");
  public static final By BTN_LOGIN = By.cssSelector("[data-testid='login-submit']");
}

public class SearchPage {
  public static final By INPUT_SEARCH = By.cssSelector("[data-testid='search-input']");
  public static final By BTN_SEARCH = By.cssSelector("[data-testid='search-submit']");
  public static final By BTN_ADD_TO_CART = By.cssSelector("[data-testid='product-add-cart']");
}

public class CartPage {
  public static final By INPUT_PROMO = By.cssSelector("[data-testid='promo-input']");
  public static final By BTN_APPLY_PROMO = By.cssSelector("[data-testid='promo-apply']");
  public static final By BTN_CHECKOUT = By.cssSelector("[data-testid='cart-checkout']");
}

public class OrderPage {
  public static final By MSG_CONFIRMATION = By.cssSelector("[data-testid='order-confirmation']");
}
    

Étape 3 : Steps métier

📝 Instructions

Créez les Steps avec méthodes métier (pas de locators) :

  • LoginSteps.seConnecter(User u)
  • SearchSteps.rechercherProduit(Product p)
  • CartSteps.appliquerPromo(PromoCode promo)
  • CheckoutSteps.validerCommande()
✅ Solution
public class LoginSteps {
  private final Screen screen;
  
  public LoginSteps(Screen s) { this.screen = s; }
  
  public void seConnecter(User user) {
    screen.type(LoginPage.USER, user.getEmail());
    screen.type(LoginPage.PASSWORD, user.getPassword());
    screen.click(LoginPage.BTN_LOGIN);
  }
}

public class SearchSteps {
  private final Screen screen;
  
  public SearchSteps(Screen s) { this.screen = s; }
  
  public void rechercherProduit(Product product) {
    screen.type(SearchPage.INPUT_SEARCH, product.getName());
    screen.click(SearchPage.BTN_SEARCH);
  }
  
  public void ajouterAuPanier() {
    screen.click(SearchPage.BTN_ADD_TO_CART);
  }
}

public class CartSteps {
  private final Screen screen;
  
  public CartSteps(Screen s) { this.screen = s; }
  
  public void appliquerPromo(PromoCode promo) {
    screen.type(CartPage.INPUT_PROMO, promo.getCode());
    screen.click(CartPage.BTN_APPLY_PROMO);
  }
}

public class CheckoutSteps {
  private final Screen screen;
  
  public CheckoutSteps(Screen s) { this.screen = s; }
  
  public void validerCommande() {
    screen.click(CartPage.BTN_CHECKOUT);
    // Logique paiement via Port
  }
  
  public boolean confirmationAffichee() {
    return screen.isVisible(OrderPage.MSG_CONFIRMATION);
  }
}
    

Étape 4 : Builders de données

📝 Instructions

Créez les Builders :

  • UserBuilder : email unique (timestamp), password
  • ProductBuilder : nom, prix
  • PromoCodeBuilder : code, réduction %
✅ Solution
public class UserBuilder {
  private String email = "user_" + System.currentTimeMillis() + "@test.com";
  private String password = "Test@123";
  
  public static UserBuilder aUser() { return new UserBuilder(); }
  
  public UserBuilder withEmail(String e) { this.email = e; return this; }
  public UserBuilder withPassword(String p) { this.password = p; return this; }
  
  public User build() {
    User u = new User();
    u.setEmail(email);
    u.setPassword(password);
    return u;
  }
}

public class ProductBuilder {
  private String name = "Laptop Gamer";
  private double price = 999.99;
  
  public static ProductBuilder aProduct() { return new ProductBuilder(); }
  
  public ProductBuilder withName(String n) { this.name = n; return this; }
  public ProductBuilder withPrice(double p) { this.price = p; return this; }
  
  public Product build() {
    Product p = new Product();
    p.setName(name);
    p.setPrice(price);
    return p;
  }
}

public class PromoCodeBuilder {
  private String code = "PROMO" + System.currentTimeMillis();
  private int discount = 10;
  
  public static PromoCodeBuilder aPromoCode() { return new PromoCodeBuilder(); }
  
  public PromoCodeBuilder withCode(String c) { this.code = c; return this; }
  public PromoCodeBuilder withDiscount(int d) { this.discount = d; return this; }
  
  public PromoCode build() {
    PromoCode pc = new PromoCode();
    pc.setCode(code);
    pc.setDiscount(discount);
    return pc;
  }
}
    

Étape finale : Test Clean QA complet

📝 Instructions

Réécrivez le test en combinant tout :

  • Utilisez les Builders pour les données
  • Composez avec les Steps
  • Assertions explicites
  • Aucun locator dans le test
✅ Solution complète
@Test
public void testAchatAvecPromoCleanQA() {
  // Setup
  Screen screen = new Screen(driver);
  LoginSteps login = new LoginSteps(screen);
  SearchSteps search = new SearchSteps(screen);
  CartSteps cart = new CartSteps(screen);
  CheckoutSteps checkout = new CheckoutSteps(screen);
  
  // Data
  User user = UserBuilder.aUser().build();
  Product product = ProductBuilder.aProduct().withName("Laptop Gamer").build();
  PromoCode promo = PromoCodeBuilder.aPromoCode().withCode("WINTER25").withDiscount(25).build();
  
  // Flow métier (100% lisible)
  login.seConnecter(user);
  search.rechercherProduit(product);
  search.ajouterAuPanier();
  cart.appliquerPromo(promo);
  checkout.validerCommande();
  
  // Assertion explicite
  assertTrue(checkout.confirmationAffichee(), 
    "Le message de confirmation doit être affiché après paiement");
}
    

Partie 4 : Bonus - Aller plus loin 🚀

Challenges supplémentaires :
  1. Ajoutez un Port Payment (UI vs API) avec adapter interchangeable
  2. Implémentez des screenshots automatiques en cas d'échec
  3. Créez une matrice CI/CD pour tester sur Chrome + Firefox
  4. Ajoutez Allure Reports pour visualiser les résultats
  5. Gérez plusieurs environnements (dev/staging) via config

Partie 5 : Auto-évaluation ✓

Checklist de validation :
  • ☐ Aucun Thread.sleep() dans le code
  • ☐ Aucun locator (By) dans les tests
  • ☐ Données générées dynamiquement (pas de valeurs en dur)
  • ☐ Steps 100% métier (noms compréhensibles par PO)
  • ☐ Assertions explicites (pas de contains générique)
  • ☐ Code maintenable (changement UI = 1 seul fichier à modifier)
  • ☐ Idempotent (peut rejouer le test sans conflit)
🎓 Félicitations !
Vous avez transformé un test spaghetti en framework Clean QA robuste. Ce pattern s'applique à tous vos projets de test, quelle que soit la techno (Web, Mobile, API, LEGACY). Vous êtes maintenant prêt à industrialiser vos tests !

❌ 50 erreurs Clean QA courantes

Ce catalogue recense les erreurs les plus fréquentes rencontrées lors de la construction et de l'évolution de frameworks de tests automatisés selon les principes Clean QA. Qu'il s'agisse d'un projet Web, Mobile, API ou LEGACY , ces problèmes apparaissent dès qu'on néglige l'architecture : absence de Core robuste, Steps couplés à la technique, données hardcodées, waits approximatifs, assertions vagues, ou encore méconnaissance des patterns Ports & Adapters.

Chaque carte identifie un anti-pattern précis avec son symptôme typique, sa cause racine et la solution Clean QA éprouvée. Les erreurs sont organisées en 9 catégories :

  • Architecture & Structure — Core, Page Objects, modularité
  • Données & Builders — génération dynamique, Factories, cleanup
  • Waits & Synchronisation — WebDriverWait, retry, timeouts cohérents
  • Steps & Vocabulaire métier — nommage, abstraction, assertions explicites
  • Ports & Adapters — découplage UI/API, configuration externe
  • Observabilité & Debug — screenshots, logs, artefacts
  • Spécificités par type — Image/OCR avec SikuliX, Mobile avec Appium, API avec RestAssured/Postman
  • Idempotence & Isolation — tests autonomes, environnements éphémères
  • Assertions & Validations — contrats structurés, messages clairs

L'objectif n'est pas de créer de la peur, mais de vous armer face aux pièges classiques qui transforment un framework prometteur en cauchemar de maintenance. Ces 50 erreurs sont le fruit de centaines de projets réels : en les connaissant, vous économiserez des mois de refactoring douloureux et bâtirez dès le départ des tests robustes, lisibles et pérennes.

📐 Catégorie 1 : Architecture & Structure

❌ 1. Locators hardcodés dans les tests

Symptôme : Changement UI → 50+ tests cassés.

Cause : By.id("btn") directement dans les tests.

Solution : Page Objects centralisés, Steps sans locators.

❌ 2. Pas de séparation Core/Steps

Symptôme : driver.findElement() éparpillé partout.

Cause : Pas de couche Core abstraite.

Solution : ScreenCore avec click(), type(), waitFor().

❌ 3. XPath fragiles (indexés)

Symptôme : //div[1]/button[3] casse constamment.

Cause : Sélecteurs positionnels instables.

Solution : data-testid, accessibility-id, resource-id.

❌ 4. Duplication de code entre tests

Symptôme : Login copié-collé 50 fois.

Cause : Pas de factorisation via Steps.

Solution : LoginSteps.seConnecter() réutilisable.

❌ 5. Framework monolithique

Symptôme : Un seul fichier de 3000 lignes.

Cause : Pas de découpage modulaire.

Solution : Core → Steps → Tests, responsabilités claires.

🗄️ Catégorie 2 : Données & Builders

✅ 6. Données hardcodées

Symptôme : "alice@test.com" existe déjà → test échoue.

Cause : Email statique dans tous les tests.

Solution : UserBuilder avec timestamp unique.

✅ 7. Datasets dupliqués

Symptôme : JSON copiés dans 20 fichiers.

Cause : Pas de Factory/Catalog.

Solution : ArticlesCatalog.articleStandard().

✅ 8. Pas de génération dynamique

Symptôme : Conflits de données entre exécutions.

Cause : ID/EAN fixes.

Solution : UUID/timestamp dans Builders.

✅ 9. CSV/JSON en dur

Symptôme : Maintenance cauchemardesque.

Cause : Fichiers statiques non versionnés.

Solution : Builders + Factories programmatiques.

✅ 10. Absence de cleanup

Symptôme : Base polluée, tests interdépendants.

Cause : Pas de tearDown/DELETE.

Solution : Cleanup systématique ou environnements éphémères.

⏱️ Catégorie 3 : Waits & Synchronisation

🔍 11. Thread.sleep() partout

Symptôme : Tests lents + flaky.

Cause : Attentes arbitraires (2000ms).

Solution : WebDriverWait conditionnels dans Core.

🔍 12. Timeouts incohérents

Symptôme : 5s ici, 30s là, instabilité.

Cause : Pas de constante globale.

Solution : DEFAULT_TIMEOUT = 10s dans Core.

🔍 13. Pas de retry automatique

Symptôme : Échec sur timeout réseau ponctuel.

Cause : Aucun mécanisme de retry.

Solution : Retry 3x avec backoff (API/OCR).

🔍 14. Animations non désactivées (mobile)

Symptôme : Timing instable sur Android/iOS.

Cause : Animations système actives.

Solution : ADB settings animator_duration_scale 0.

🔍 15. Waits sur mauvais critère

Symptôme : Attend que l'élément existe, mais pas qu'il soit cliquable.

Cause : visibilityOf au lieu de elementToBeClickable.

Solution : Conditions adaptées (clickable, visible, text présent).

💼 Catégorie 4 : Steps & Vocabulaire métier

💡 16. Steps techniques au lieu de métier

Symptôme : clickButton(), enterText() dans tests.

Cause : Vocabulaire technique exposé.

Solution : seConnecter(), rechercherProduit().

💡 17. Locators dans Steps

Symptôme : By.id() directement dans LoginSteps.

Cause : Couplage Steps ↔ UI.

Solution : Steps appellent Core/Pages uniquement.

💡 18. Steps sans assertions

Symptôme : Actions sans vérification → faux positifs.

Cause : Steps "silencieux".

Solution : Au moins 1 assertion par scénario métier.

💡 19. Nommage obscur

Symptôme : test_45_v2() incompréhensible.

Cause : Pas de convention naming.

Solution : authentification_echoue_si_mdp_invalide().

💡 20. Steps trop granulaires

Symptôme : 20 steps pour un login.

Cause : Découpage excessif.

Solution : Regrouper en actions métier cohérentes.

🔌 Catégorie 5 : Ports & Adapters

⚙️ 21. Couplage UI/API dans tests

Symptôme : Impossible de switcher canal sans réécrire.

Cause : Logique métier mélangée avec technique.

Solution : PaymentPort + UiAdapter/ApiAdapter.

⚙️ 22. Pas d'abstraction canal

Symptôme : Tests dupliqués UI vs API.

Cause : Pas de Port pour abstraire.

Solution : ValidationPort avec adapters interchangeables.

⚙️ 23. Configuration hardcodée

Symptôme : if (useApi) {...} dans le test.

Cause : Sélection adapter en dur.

Solution : Config externe (properties/env vars).

⚙️ 24. Adapters non testés

Symptôme : ApiAdapter casse en prod.

Cause : Pas de tests unitaires sur adapters.

Solution : Tests dédiés par adapter.

⚙️ 25. Dépendances circulaires

Symptôme : Steps → Ports → Steps.

Cause : Architecture mal pensée.

Solution : Dépendances unidirectionnelles (Tests → Steps → Ports → Core).

🔍 Catégorie 6 : Observabilité & Debug

❌ 26. Pas de screenshots automatiques

Symptôme : Test échoue, impossible de voir pourquoi.

Cause : Pas de capture en cas d'échec.

Solution : Listener qui screenshot + upload artefacts CI.

❌ 27. Logs inexistants

Symptôme : Debug aveugle, 1h+ par échec.

Cause : Pas de logging structuré.

Solution : Logger.info("[Step] Action + résultat").

❌ 28. Pas de HAR files (network)

Symptôme : API calls mystérieux.

Cause : Pas de capture réseau.

Solution : BrowserMob/DevTools pour sauver HAR.

❌ 29. Messages d'erreur vagues

Symptôme : "Test failed" sans contexte.

Cause : Assertions sans message explicite.

Solution : assertTrue(cond, "Le total doit être correct").

❌ 30. Artefacts non uploadés

Symptôme : Rapports perdus après run.

Cause : Pas d'upload-artifact dans CI.

Solution : GitHub Actions upload-artifact.

🎯 Catégorie 7 : Spécificités par type

✅ 31. Images SikuliX dispersées

Symptôme : "btn_*.png" introuvables.

Cause : Chemins relatifs partout.

Solution : Images centralisé avec Pattern stables.

✅ 32. OCR sans retry

Symptôme : Texte vide aléatoirement.

Cause : Pas de retry sur serveur OCR.

Solution : Retry 3x avec backoff.

✅ 33. Zones OCR aléatoires

Symptôme : Reconnaissance instable.

Cause : Recherche plein écran.

Solution : Regions.TICKET_ZONE fixes.

🔍 34. XPath indexés mobile

Symptôme : //Button[1] casse tout le temps.

Cause : Sélecteurs positionnels.

Solution : MobileBy.AccessibilityId / resource-id.

🔍 35. Permissions non gérées

Symptôme : Test bloqué sur popup permission.

Cause : Pas de PermissionHelper.

Solution : accepterPermissionCamera() automatisé.

🔍 36. Pas de test offline

Symptôme : Bugs offline non détectés.

Cause : Tous les tests online uniquement.

Solution : NetworkHelper.disableNetwork().

💡 37. JSON en String brut

Symptôme : Typos, pas de validation IDE.

Cause : Payloads manuels.

Solution : POJOs + OrderBuilder.

💡 38. Pas de validation contrat

Symptôme : 50 tests cassent sur changement API.

Cause : Pas de JSON Schema.

Solution : matchesJsonSchema() centralisé.

💡 39. IDs statiques API

Symptôme : "customerId": "12345" échoue.

Cause : Pas de chaînage de requêtes.

Solution : Variables env Postman / Steps Java.

💡 40. Headers oubliés

Symptôme : Échecs intermittents API.

Cause : Content-Type, X-Request-Id manquants.

Solution : Assertions headers dans contrat.

🔒 Catégorie 8 : Idempotence & Isolation

⚙️ 41. Tests interdépendants

Symptôme : Test B échoue si Test A ne tourne pas avant.

Cause : Données partagées.

Solution : Chaque test crée ses données (setup).

⚙️ 42. Pas de cleanup

Symptôme : Base polluée, conflits entre runs.

Cause : tearDown absent.

Solution : DELETE systématique ou DB éphémère.

⚙️ 43. État global partagé

Symptôme : Variable statique modifiée par un test.

Cause : Singleton mal géré.

Solution : Isolation complète par test.

⚙️ 44. Ordre d'exécution implicite

Symptôme : Tests passent en séquentiel, échouent en parallèle.

Cause : Dépendance cachée.

Solution : Tests 100% autonomes.

⚙️ 45. Cache non nettoyé

Symptôme : Ancien état persiste entre runs.

Cause : localStorage/cookies non purgés.

Solution : driver.manage().deleteAllCookies().

✔️ Catégorie 9 : Assertions & Validations

❌ 46. Assertions vagues

Symptôme : assertTrue(page.contains("OK")).

Cause : Validation trop générique.

Solution : confirmationAffichee() explicite.

❌ 47. Pas d'assertion du tout

Symptôme : Test vert sans rien valider.

Cause : Steps silencieux.

Solution : Au moins 1 assert par scénario.

❌ 48. Faux positifs

Symptôme : Test passe malgré bug évident.

Cause : Assertion trop permissive.

Solution : Valider état précis (montant exact, texte complet).

❌ 49. Pas de validation structurelle

Symptôme : JSON malformé accepté.

Cause : Pas de schema validation.

Solution : JSON Schema + assertions contractuelles.

❌ 50. Messages d'erreur inutiles

Symptôme : "expected true but was false".

Cause : Pas de message personnalisé.

Solution : "Le prix TTC doit correspondre" dans assert.

🎯 50 erreurs Clean QA cataloguées !
Ces patterns d'erreurs couvrent tous les aspects : architecture, données, waits, steps, ports, observabilité, spécificités (Legacy/Mobile/API), idempotence et assertions. À chaque erreur sa solution Clean QA éprouvée.