De zéro à expert · Par Julien MER — QA Architect · 20 ans de terrain · 230+ tests en production
1. Le problème réel
Depuis 20 ans, les outils d'automatisation reposent sur une hypothèse implicite : l'application expose une structure programmatique interrogeable. Un DOM HTML, un arbre d'accessibilité Android, une API. Selenium cherche un id. Playwright attend un sélecteur CSS. Appium interroge un arbre de vues.
Cette hypothèse fonctionne pour les applications web modernes et les apps mobiles bien construites. Elle s'effondre dès qu'on sort de ce cadre. Et dans l'industrie, on en sort bien plus souvent qu'on ne le croit.
🟣 Retour terrain : dans les secteurs critiques — défense, biotech, retail, aéronautique — la majorité des applications à tester n'ont ni DOM, ni API, ni sélecteur. Des interfaces legacy vieilles de 20 ans, accessibles uniquement via VNC sur des réseaux contraints. Aucun outil classique ne peut y toucher. Le visual testing est la seule solution.
Ce que tu vas rencontrer sur le terrain
Un ERP legacy des années 90 avec une interface C++ sans hook d'automatisation
Un logiciel de caisse propriétaire dont personne ne connaît le code source
Une application Citrix qui n'est qu'un flux de pixels sur le réseau
Un terminal Linux affiché via VNC dans un datacenter distant
Un écran embarqué sur un équipement industriel
⚠️ La question que personne ne pose en entretien : "Comment tu testes une application qui n'a pas de sélecteur ?" C'est pourtant le problème numéro 1 en secteurs critiques.
2. Quand les outils classiques échouent
Contexte
Pourquoi c'est bloquant
Exemples
Desktop natif
Pas de DOM, pas de sélecteur CSS
WPF, Swing, Qt, Delphi
ERP / logiciel métier propriétaire
Aucun hook d'automatisation exposé
SAP GUI, Oracle Forms, CEGID
Citrix / VNC / RDP
Flux de pixels uniquement
Sessions desktop distantes, mainframes
Legacy Linux
Interface graphique via X11 ou VNC uniquement
Outils internes déployés depuis 15+ ans
Applications embarquées
Pas d'OS standard, interface sur écran dédié
Bornes, équipements industriels
Canvas HTML5
Pas d'élément interactif interrogeable
Graphiques, éditeurs visuels
✅ La règle de décision :
— Il y a un DOM accessible → Playwright / Selenium
— Il n'y a pas de DOM ou on veut valider l'affichage visuel → Visual Testing
— On veut lire du texte rendu visuellement → OCR
Les deux coexistent souvent dans un même projet.
3. Le principe du visual testing
Le visual testing inverse la logique : au lieu de chercher un élément dans une structure programmatique, on regarde l'écran comme un humain.
🖼️ Reconnaissance d'images
On capture une image de référence d'un bouton. Le moteur cherche cette image à l'écran par template matching. Quand il la trouve, il peut cliquer, hoverer, interagir.
🔤 Reconnaissance de texte (OCR)
Le moteur lit le texte affiché à l'écran, même s'il est rendu sous forme d'image. OCR extrait le texte lisible par un humain.
Le template matching
Le template matching compare pixel par pixel une image de référence avec les régions de l'écran et calcule un score de similarité entre 0 et 1.
⚠️ Le seuil par défaut est trop bas. SikuliX met 0.7 par défaut — ce qui génère des faux positifs en production. En pratique, 0.90 à 0.95 est le bon réglage.
🟣 Choix terrain : SikuliX / OculiX s'impose pour les projets enterprise — intégration Maven/Gradle native, Jenkins, VNC natif, zéro SaaS. C'est la seule solution production viable à coût zéro.
5. Pourquoi SikuliX + double moteur OCR
SikuliX embarque Tesseract — solide sur du texte standard en latin sur fond blanc. Mais il échoue sur texte coloré, polices non standard, captures basse résolution, caractères asiatiques.
La stratégie : un double moteur OCR en cascade.
🔤 Moteur 1 — Tesseract
Rapide (50ms), intégré à SikuliX, excellent sur texte simple. Tentative en premier.
🔤 Moteur 2 — PaddleOCR
Basé sur deep learning (300ms), performant sur cas complexes. Déclenché si Tesseract renvoie une confiance insuffisante.
✅ Résultat en production sur 230+ tests : 89% des lectures via Tesseract (52ms en moyenne), 11% via PaddleOCR (310ms). Fiabilité globale : 99.3%.
6. Ce que tu vas savoir faire
✅ Structurer le projet
Arborescence, dossier d'images, classes de constantes — avant même d'écrire un test.
Lire du texte sur n'importe quel fond, valider des valeurs numériques.
✅ VNC et CI/CD
Docker headless, Jenkins pipeline, tests sur serveurs distants.
1. Arborescence du projet
Avant d'écrire une seule ligne de test, la structure du projet doit être en place. C'est ce qui fait la différence entre un framework maintenable et un chaos de fichiers éparpillés.
Voici la structure recommandée, indépendante du framework utilisé (Maven, Gradle, Katalon) :
💡 Le dossier Pattern/ est le cœur du framework. Toutes les images de référence y vivent. Jamais un chemin d'image en dur dans un test — toujours via une constante.
Pourquoi cette séparation ?
Pattern/ — les images de référence versionées dans Git. Si l'interface change, on ne touche qu'aux images, pas au code.
Keywords/ — les managers techniques qui encapsulent SikuliX. Un test n'importe jamais SikuliX directement.
Constants/ — la colle entre les deux. Centralise les chemins pour qu'un renommage de fichier ne casse qu'un seul endroit.
2. Capturer les images de référence avec SikuliX IDE
C'est l'étape que tous les débutants ratent. Avant d'écrire du code, il faut créer les images de référence. La qualité de ces images détermine 80% de la fiabilité du framework.
Étape 1 — Ouvrir SikuliX IDE
SikuliX IDE est livré avec l'archive SikuliX. Lance runsikulix.cmd (Windows) ou runsikulix.sh (Linux/Mac). L'IDE s'ouvre avec un éditeur de script intégré.
Utiliser l'outil de capture intégré
Dans SikuliX IDE, clique sur le bouton 📷 "Take screenshot" dans la barre d'outils
L'écran se fige et un réticule apparaît
Dessine un rectangle autour de l'élément à capturer — uniquement l'élément, pas le fond
Relâche — l'image est automatiquement insérée dans le script
Clique-droit sur l'image dans le script → "Save as" → sauvegarde dans Pattern/
Les règles d'une bonne image de référence
📐 Taille minimale
Capture uniquement l'élément, pas son entourage. Un bouton : juste le bouton. Plus l'image est petite et précise, plus le matching est fiable.
🖥️ Résolution fixe
Capture toujours à la même résolution que l'environnement d'exécution. Une image capturée en 4K ne matchera pas sur un écran FullHD.
🎯 Élément stable
Évite les zones dynamiques : compteurs, horodatages, données variables. Préfère les labels, icônes, contours de boutons.
📝 Nommage explicite
btn_valider_panier.png pas img1.png. Le nom doit permettre de savoir ce que c'est sans ouvrir le fichier.
🔴 Piège classique — L'image trop grande : si tu captures toute la fenêtre au lieu de juste le bouton, SikuliX cherchera cette grande image à l'écran. Au moindre déplacement de fenêtre ou changement de fond, le matching échoue. Règle absolue : l'image doit être la plus petite possible tout en restant unique à l'écran.
Étape 2 — Valider l'image capturée
Avant de committer l'image dans le projet, valide-la dans SikuliX IDE :
// Dans l'IDE SikuliX, tape ce mini-script et lance-le
Screen s = new Screen();
if (s.exists("Pattern/Actions/btn_valider.png", 3) != null) {
s.highlight(2); // Surligne ce qui a été trouvé
System.out.println("Image valide — score : " + s.find("Pattern/Actions/btn_valider.png").getScore());
} else {
System.out.println("Image non trouvée — recapturer");
}
Le score retourné doit être supérieur à 0.92. En dessous, recapture l'image.
3. La classe de constantes d'images
C'est LE fichier le plus important du framework. Il centralise tous les chemins d'images. Jamais un chemin en dur dans un test. Jamais.
Pourquoi c'est indispensable
Sans cette classe, si tu renommes btn_valider.png en btn_confirmer.png, tu dois chercher et remplacer dans tous tes tests. Avec cette classe, tu modifies un seul endroit.
/**
* Centralise tous les chemins d'images de référence du framework.
* RÈGLE : aucun chemin d'image ne doit apparaître directement dans un test.
*/
public class ImageConstants {
// ── Répertoire racine des images ──
private static final String BASE = "Pattern/";
// ── Navigation ──
public static final String MENU_PRINCIPAL = BASE + "Navigation/menu_principal.png";
public static final String BTN_RETOUR = BASE + "Navigation/btn_retour.png";
public static final String ICONE_ACCUEIL = BASE + "Navigation/icone_accueil.png";
public static final String ONGLET_STOCK = BASE + "Navigation/onglet_stock.png";
// ── Actions génériques ──
public static final String BTN_VALIDER = BASE + "Actions/btn_valider.png";
public static final String BTN_ANNULER = BASE + "Actions/btn_annuler.png";
public static final String BTN_SUPPRIMER = BASE + "Actions/btn_supprimer.png";
public static final String BTN_NOUVEAU = BASE + "Actions/btn_nouveau.png";
public static final String BTN_RECHERCHER = BASE + "Actions/btn_rechercher.png";
// ── Statuts et indicateurs ──
public static final String ICONE_SUCCES = BASE + "Statuts/icone_succes.png";
public static final String ICONE_ERREUR = BASE + "Statuts/icone_erreur.png";
public static final String SPINNER = BASE + "Statuts/spinner_chargement.png";
public static final String DIALOGUE_CONFIRM = BASE + "Statuts/dialogue_confirmation.png";
public static final String BANDEAU_ALERTE = BASE + "Statuts/bandeau_alerte.png";
// ── Champs de saisie (labels permettant de localiser les champs) ──
public static final String LABEL_IDENTIFIANT = BASE + "Champs/label_identifiant.png";
public static final String LABEL_MOT_DE_PASSE = BASE + "Champs/label_mot_de_passe.png";
public static final String LABEL_MONTANT = BASE + "Champs/label_montant.png";
// Constructeur privé — classe utilitaire, pas d'instanciation
private ImageConstants() {}
}
Une constante par module
Pour les modules applicatifs complexes, crée une constante dédiée :
/**
* Constantes d'images spécifiques au module de gestion des stocks.
*/
public class StockImageConstants {
private static final String BASE = "Pattern/ModuleStock/";
public static final String TITRE_MODULE = BASE + "titre_module_stock.png";
public static final String BTN_NOUVEAU_MOUVEMENT = BASE + "btn_nouveau_mouvement.png";
public static final String FORMULAIRE_MOUVEMENT = BASE + "formulaire_mouvement.png";
public static final String TABLEAU_RESULTATS = BASE + "tableau_resultats.png";
public static final String LIGNE_STOCK = BASE + "ligne_stock_template.png";
private StockImageConstants() {}
}
✅ Résultat : dans un test, tu écris ImageConstants.BTN_VALIDER — jamais "Pattern/Actions/btn_valider.png". Si l'image change de nom ou de dossier, une seule ligne à modifier dans toute la codebase.
4. Organisation par module — Scaler le framework
Sur un projet de 100+ tests, une seule classe ImageConstants devient ingérable. On découpe par domaine fonctionnel.
Une classe de constantes = un domaine fonctionnel cohérent. Si tu dois chercher dans deux classes pour écrire un test, c'est que le découpage est mal fait.
5. Les classes métier
Au-dessus des managers techniques, on peut créer des classes métier qui expriment des actions fonctionnelles. Ce n'est pas obligatoire mais cela améliore la lisibilité des tests.
⚠️ Important : les classes métier sont optionnelles. Les managers techniques suffisent pour écrire des tests complets. Les classes métier sont un bonus de lisibilité, pas une obligation architecturale.
/**
* Exemple de classe métier — Login.
* Regroupe les actions liées à la connexion en un vocabulaire fonctionnel.
* Elle utilise les managers techniques — jamais SikuliX directement.
*/
public class LoginActions {
private final ScreenOperationsManager som;
public LoginActions(ScreenOperationsManager som) {
this.som = som;
}
public boolean seConnecter(String identifiant, String motDePasse) {
// Attendre l'écran de login
if (!som.waitForElement(LoginImageConstants.ECRAN_LOGIN, 20)) return false;
// Saisir les identifiants
som.insertText(LoginImageConstants.CHAMP_IDENTIFIANT, identifiant);
som.typeTextWithEnter(motDePasse);
// Cliquer sur "Se connecter"
som.clickOn(LoginImageConstants.BTN_SE_CONNECTER);
// Vérifier le succès
return som.waitForElement(ImageConstants.ICONE_ACCUEIL, 10);
}
public void seDeconnecter() {
som.clickOn(ImageConstants.MENU_PRINCIPAL);
som.waitForElement(LoginImageConstants.BTN_DECONNEXION, 5);
som.clickOn(LoginImageConstants.BTN_DECONNEXION);
}
}
Le test devient alors :
// Avec classes métier — lisible par un non-développeur
LoginActions login = new LoginActions(som);
login.seConnecter("admin", "motdepasse");
// Sans classes métier — directement avec les managers
som.waitForElement(LoginImageConstants.ECRAN_LOGIN, 20);
som.insertText(LoginImageConstants.CHAMP_IDENTIFIANT, "admin");
som.typeTextWithEnter("motdepasse");
som.clickOn(LoginImageConstants.BTN_SE_CONNECTER);
💡 Les deux approches sont valides. En pratique, commence par les managers directs. Extrais en classes métier uniquement quand une séquence est répétée dans plus de 3 tests.
Architecture d'ensemble
Le framework est structuré en managers techniques spécialisés. Chaque manager a une responsabilité unique. Un test ou une classe métier n'importe jamais SikuliX directement — il passe toujours par un manager.
┌─────────────────────────────────┐ │ Test / Classe Métier │ └────────────────┬────────────────┘ │ ┌────────────────▼────────────────┐ │ ScreenOperationsManager │ ← Point d'entrée unique └──┬──────┬──────┬──────┬────┬───┘ │ │ │ │ │ Click Wait Type OCR Region Capture Mgr Mgr Mgr Mgr Mgr Mgr │ │ │ │ │ ┌──▼──────▼──────▼──────▼────▼───┐ │ SikuliX Engine │ └─────────────────────────────────┘
Principe de délégation
ScreenOperationsManager est le seul point d'entrée exposé aux tests. Il délègue chaque type d'action au manager spécialisé correspondant. Les tests ne savent pas que ClickManager ou WaitManager existent.
// Dans ScreenOperationsManager — délégation aux managers spécialisés
public class ScreenOperationsManager {
private final ClickManager clickManager;
private final WaitManager waitManager;
private final TypeManager typeManager;
private final OCRManager ocrManager;
private final RegionManager regionManager;
private final CaptureManager captureManager;
// ThreadLocal pour la thread-safety en exécution parallèle
private static final ThreadLocal<Region> screenHolder = ThreadLocal.withInitial(Screen::new);
public ScreenOperationsManager() {
this.typeManager = new TypeManager(screenHolder);
this.clickManager = new ClickManager(screenHolder, HIGHLIGHT_DELAY, typeManager);
this.waitManager = new WaitManager(screenHolder, HIGHLIGHT_DELAY, clickManager);
this.ocrManager = new OCRManager(screenHolder);
this.regionManager = new RegionManager(screenHolder, HIGHLIGHT_DELAY, waitManager, ocrManager);
this.captureManager = new CaptureManager(screenHolder);
}
// Délégation — le test appelle clickOn, pas clickManager.clickOn
public void clickOn(String imagePath) {
clickManager.clickOn(imagePath);
}
public boolean waitForElement(String imagePath, int timeout) {
return waitManager.waitForElement(imagePath, timeout);
}
public void typeText(String text) {
typeManager.typeText(text);
}
// ... toutes les autres délégations
}
ThreadLocal — pourquoi c'est important
Le ThreadLocal<Region> garantit que chaque thread d'exécution a son propre écran. En exécution parallèle (plusieurs tests en même temps sur des VNC différents), les screens ne se mélangent pas.
ScreenOperationsManager — Point d'entrée unique
Classe façade qui expose l'API publique complète du framework. Les tests n'interagissent qu'avec cette classe. Elle instancie et délègue à chaque manager spécialisé.
🎯 Responsabilité
Façade unique, instanciation des managers, initialisation de l'écran (local ou VNC), configuration globale SikuliX.
Gère l'intégralité des clics et interactions pointer. Un clic n'est jamais appelé directement sur screenHolder.get() depuis un test — toujours via ce manager.
Certaines interfaces n'ont pas de bouton cliquable identifiable par image. Mais elles ont un label stable à côté d'une zone de saisie. clickNearImage clique à une distance et dans une direction données par rapport à une image de référence.
// Cliquer dans le champ de saisie qui est 150px à droite du label "Montant :"
som.clickNearImage(ImageConstants.LABEL_MONTANT, 0.92, 150, "right");
// Puis saisir la valeur
som.typeText("1500");
WaitManager — Attentes et assertions visuelles
Gère toutes les attentes sur des éléments visuels. C'est le manager le plus utilisé dans les tests — chaque interaction commence par une attente.
Le mode silencieux — waitForElement avec silentMode
Parfois tu veux vérifier si un élément est présent sans que son absence soit une erreur. Le mode silencieux ne loggue pas d'erreur si l'élément n'est pas trouvé.
// Mode normal — si non trouvé, log d'erreur et retourne false
boolean present = som.waitForElement(ImageConstants.DIALOGUE_CONFIRM, 3);
// Mode silencieux — vérifie sans bruit si un dialogue optionnel est apparu
boolean dialoguePresent = som.waitForElement(ImageConstants.DIALOGUE_CONFIRM, 3, true);
if (dialoguePresent) {
som.clickOn(ImageConstants.BTN_VALIDER);
}
validateFinalVisualState — la méthode de validation premium
Valide l'état visuel final d'un scénario par rapport à une image de référence capturée lors des premiers runs validés. Si l'écran ne correspond pas, le test échoue avec une capture d'échec.
// A la fin d'un scénario, valider que l'écran correspond à la référence
som.validateFinalVisualState("Connexion_Standard", "Apres_Connexion_Reussie");
// → Cherche Pattern/ScenarioElements/Connexion_Standard_Apres_Connexion_Reussie.png
// → Si non trouvé avec sim 0.99 → markFailedAndStop + capture d'échec
TypeManager — Saisie clavier
Gère toute la saisie clavier — texte simple, touches spéciales, caractères accentués, combinaisons de touches. La saisie de caractères spéciaux (@, #, €, [, ]) est une source de bugs classique selon le layout clavier.
// typeText — frappe directe dans la zone active (focus courant)
som.typeText("MonTexte");
// insertText — clique sur l'image puis frappe dans ce champ
// Utile quand le focus n'est pas garanti
som.insertText(ImageConstants.CHAMP_RECHERCHE, "MonTexte");
// typeTextWithEnter — frappe + touche Entrée (valider un formulaire)
som.typeTextWithEnter("MonTexte");
⚠️ Caractères spéciaux et layout clavier : sur un clavier AZERTY français, le @ s'obtient avec AltGr+0. SikuliX type caractère par caractère et peut mal interpréter les caractères spéciaux. Le TypeManager gère ces cas avec une table de correspondance.
OCRManager — Lecture de texte
Gère toutes les opérations de lecture de texte à l'écran via PaddleOCR (moteur principal) et Tesseract (fallback). Permet de lire, attendre et cliquer sur du texte sans image de référence.
// Si le texte est dynamique (montant variable), utilise OCR
som.waitForText("Paiement validé"); // Peu importe comment c'est rendu visuellement
som.waitForText("1 250,00 €", 10); // Attend ce montant précis à l'écran
som.clickOnText("Confirmer"); // Clique sur ce mot où qu'il soit
// Si l'élément est toujours identique visuellement, utilise l'image
som.waitForElement(ImageConstants.BTN_VALIDER, 5); // Plus rapide, plus précis
detectTextPairInActiveApp — valider deux valeurs ensemble
Cas d'usage réel : valider qu'un libellé et son montant associé apparaissent ensemble sur la même ligne d'un écran de récapitulatif.
// Vérifier que "Remise fidélité" et "-5,00 €" sont sur la même ligne
boolean ok = som.detectTextPairInActiveApp("Remise fidélité", "-5,00 €", 15.0);
// toleranceY=15 = les deux textes doivent être à moins de 15px de hauteur l'un de l'autre
RegionManager — Zones et navigation
Gère les opérations sur des régions de l'écran : recherche de zones relatives à une image, scroll, extraction de texte dans une zone précise, clic par coordonnées OCR.
findRegionBelow — localiser un champ par son label
Le pattern le plus utilisé : un label est stable (image de référence), mais le champ de saisie à côté ne l'est pas (valeur variable). findRegionBelow crée une région juste en dessous du label.
// Le champ "Quantité" est sous le label "Quantité :"
Region champQte = som.findRegionBelow(ImageConstants.LABEL_QUANTITE, 35);
champQte.click();
som.typeText("150");
clickOnInputByRowAndColumn — grille OCR
Pour les tableaux sans sélecteur, localise une cellule par l'intersection de sa ligne (texte OCR) et sa colonne (en-tête OCR), puis clique dessus.
// Cliquer sur la cellule "Montant" de la ligne "Carte bancaire"
som.clickOnInputByRowAndColumn("Moyen de paiement", "Carte bancaire", "Montant");
CaptureManager — Captures et rapport
Gère les captures d'écran pour le rapport d'exécution. Chaque échec génère automatiquement une capture horodatée dans FailureScreenshots/.
// Capture manuelle à un point clé du test
som.captureScreen(RunConfiguration.getReportFolder());
// → Sauvegarde : FailureScreenshots/NomDuScenario_20240315_143022_Error.png
// Popup de debug pendant le développement (se ferme automatiquement)
som.showAutoClosingPopup("Connexion établie — attente de l'écran principal", 2000);
1 Setup Maven — Intégrer SikuliX
Prérequis
Java 11 minimum (java -version)
Maven 3.6+ (mvn -version)
Un environnement graphique — pas un serveur headless (voir onglet CI/CD)
Sur Linux : sudo apt install libopencv-dev tesseract-ocr
⚠️ SikuliX archivé depuis mars 2026. Utilise OculiX pour les nouveaux projets. En attendant la publication Maven Central d'OculiX, utilise SikuliX 2.0.5 pour ces exercices.
import org.sikuli.script.Screen;
public class SetupVerification {
public static void main(String[] args) {
Screen ecran = new Screen();
System.out.println("Résolution : " + ecran.w + "x" + ecran.h);
ecran.capture().save("/tmp/", "verification_setup.png");
System.out.println("Setup OK — capture sauvegardée");
}
}
Test attendu
Lance mvn compile exec:java
Tu vois : Résolution : 1920x1080 (ou ta résolution)
Le fichier /tmp/verification_setup.png existe et montre ton écran
🔴 UnsatisfiedLinkError : SikuliX charge des bibliothèques natives. Sur Ubuntu : sudo apt install libopencv-dev tesseract-ocr. Sur Windows : vérifier que le chemin du projet ne contient pas d'espaces.
2 Seuils et Pattern — Calibrer la précision
La table de décision des seuils
Seuil
Comportement
Risque
0.5 – 0.7
Matche n'importe quoi qui ressemble vaguement
🔴 Faux positifs garantis en prod
0.7 (défaut)
Acceptable en démo, inacceptable en prod
🟡 Faux positifs occasionnels
0.90 – 0.95
Match fiable sur éléments stables
✅ Recommandé en production
0.98 – 1.0
Identique pixel par pixel
🔴 Faux négatifs dès que l'interface change d'un pixel
import org.sikuli.script.*;
// ── Configuration globale — au démarrage du framework ──
Settings.MinSimilarity = 0.92;
Settings.WaitScanRate = 15; // Scans/seconde pendant wait() — défaut 3
Settings.MoveMouseDelay = 0.0f; // Supprimer l'animation du curseur en CI
// ── Pattern personnalisé par image ──
Pattern btnValider = new Pattern(ImageConstants.BTN_VALIDER).similar(0.95);
Pattern iconeChargement = new Pattern(ImageConstants.SPINNER).similar(0.80);
// Logger le score réel pour calibrer
Match result = ecran.find(btnValider);
System.out.println("Score réel : " + result.getScore());
Méthode de calibration
Commencer à 0.70 et logger le score (match.getScore())
Par défaut, SikuliX scanne tout l'écran. Sur un écran 1920x1080, c'est 2 millions de pixels. Avec une région, on réduit à quelques milliers.
💡 Recherche plein écran ≈ 150-300ms. Région 400x300 ≈ 20-40ms. Sur 230 tests avec 5 interactions chacun : plusieurs minutes gagnées par run CI.
// Régions fixes — zones stables de l'interface
Region zoneMenu = new Region(0, 0, 300, 900); // Panneau gauche
Region zoneContenu = new Region(300, 0, 1200, 900); // Zone principale
Region zoneFooter = new Region(0, 900, 1920, 180); // Barre du bas
// Chercher dans la zone concernée uniquement
zoneMenu.click(ImageConstants.BTN_MENU_FICHIER);
zoneContenu.wait(ImageConstants.FENETRE_RESULTAT, 5);
// ── Région dynamique — calculée à partir d'un élément trouvé ──
Match labelNom = ecran.find(ImageConstants.LABEL_NOM);
Region champSaisie = new Region(
labelNom.x + labelNom.w + 10,
labelNom.y - 2,
200,
labelNom.h + 4
);
champSaisie.click();
champSaisie.type("Julien MER");
4 Wait, exists, find — Choisir la bonne méthode
Méthode
Bloquant
Si non trouvé
Usage
wait(img, timeout)
Oui
FindFailed
Prérequis — si absent, le test ne peut pas continuer
find(img)
Oui
FindFailed
L'élément doit être là maintenant, pas de raison d'attendre
exists(img, timeout)
Oui mais silencieux
null
Vérification optionnelle — l'élément peut ne pas être là
waitVanish(img, timeout)
Oui
FindFailed
Attendre qu'un spinner, un dialogue disparaisse
⚠️ Erreur classique : utiliser find() là où il faut wait(). Si l'élément met 2 secondes à apparaître (animation, chargement réseau), find() lève une exception immédiatement. En production, utilise presque toujours wait().
5 Gestion d'erreurs — Rendre les tests robustes
Retry avec backoff exponentiel
public Match trouverAvecRetry(Screen ecran, String image, int tentativesMax) {
int tentative = 0;
while (tentative < tentativesMax) {
try {
return ecran.find(image);
} catch (FindFailed e) {
tentative++;
if (tentative == tentativesMax)
throw new RuntimeException("Élément non trouvé après " + tentativesMax + " tentatives : " + image, e);
int attente = (int) Math.pow(2, tentative) * 500; // 500ms, 1s, 2s...
try { Thread.sleep(attente); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
}
}
return null;
}
Gérer les dialogues parasites
// En début de test — fermer les dialogues qui pourraient bloquer
String[] dialoguesConnus = {
ImageConstants.DIALOGUE_MISE_A_JOUR,
ImageConstants.DIALOGUE_SESSION_EXPIREE
};
for (String dialogue : dialoguesConnus) {
if (ecran.exists(dialogue, 1) != null) {
ecran.click(ImageConstants.BTN_FERMER_DIALOGUE);
}
}
1 Tesseract natif — Lire du texte
Prérequis
Tesseract est embarqué dans SikuliX sur Windows — aucune installation
Linux : sudo apt install tesseract-ocr tesseract-ocr-fra
Mac : brew install tesseract tesseract-lang
import org.sikuli.script.*;
// ── Lire le texte dans une zone précise ──
Region zonePrix = new Region(500, 300, 200, 50);
String texte = zonePrix.text();
// Nettoyer (espaces parasites, retours à la ligne)
String propre = texte.trim().replaceAll("\\s+", " ");
System.out.println("Lu : '" + propre + "'");
// ── Valider ──
if (propre.contains("12.50") || propre.contains("12,50")) {
System.out.println("✅ Prix correct");
}
// ── Cliquer sur du texte ──
Match cible = ecran.findText("Valider commande");
if (cible != null) ecran.click(cible);
🔴 Tesseract échoue sur : texte blanc sur fond coloré, polices décoratives, rotation, petites polices, bruit dans l'image, caractères spéciaux. → Utiliser le double moteur.
2 PaddleOCR — Le moteur IA
# Installation
pip install paddlepaddle paddleocr
from paddleocr import PaddleOCR
from PIL import ImageGrab
import numpy as np
ocr = PaddleOCR(use_angle_cls=True, lang='fr', show_log=False)
# ── Lire une zone de l'écran ──
capture = ImageGrab.grab(bbox=(500, 300, 700, 350))
img = np.array(capture)
import cv2
img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
resultats = ocr.ocr(img_bgr, cls=True)
for ligne in resultats[0]:
texte = ligne[1][0]
confiance = ligne[1][1]
print(f"'{texte}' — {confiance:.0%}")
Cas
Tesseract
PaddleOCR
Texte noir sur fond blanc
✅ 99%
✅ 99%
Texte blanc sur fond rouge
❌ 20%
✅ 95%
Police LCD (afficheurs numériques)
⚠️ 60%
✅ 92%
Image basse résolution
⚠️ 45%
✅ 80%
Temps de traitement
✅ 50ms
⚠️ 300ms
3 Double moteur — L'architecture en cascade
class DoubleMoteurOCR:
"""Tesseract en premier (rapide). PaddleOCR si confiance insuffisante."""
SEUIL = 0.80
def __init__(self):
import easyocr
self.reader = easyocr.Reader(['fr', 'en'], gpu=False, verbose=False)
def lire_zone(self, x1, y1, x2, y2):
from PIL import ImageGrab
import numpy as np
capture = ImageGrab.grab(bbox=(x1, y1, x2, y2))
img = np.array(capture)
# Tentative 1 — Tesseract
r = self._tesseract(img)
if r['confiance'] >= self.SEUIL:
r['moteur'] = 'tesseract'
return r
print(f"Tesseract {r['confiance']:.0%} → fallback EasyOCR")
# Tentative 2 — EasyOCR
r = self._easyocr(img)
r['moteur'] = 'easyocr'
return r
def _tesseract(self, img):
import pytesseract
from PIL import Image
data = pytesseract.image_to_data(
Image.fromarray(img), lang='fra+eng',
output_type=pytesseract.Output.DICT
)
mots, confs = [], []
for i, t in enumerate(data['text']):
c = data['conf'][i]
if t.strip() and c > 0:
mots.append(t)
confs.append(c / 100.0)
return {
'texte': ' '.join(mots).strip(),
'confiance': sum(confs) / len(confs) if confs else 0.0
}
def _easyocr(self, img):
res = self.reader.readtext(img)
if not res: return {'texte': '', 'confiance': 0.0}
return {
'texte': ' '.join(r[1] for r in res).strip(),
'confiance': sum(r[2] for r in res) / len(res)
}
# Utilisation
moteur = DoubleMoteurOCR()
r = moteur.lire_zone(600, 350, 800, 390)
print(f"'{r['texte']}' ({r['confiance']:.0%}) via {r['moteur']}")
🟣 Résultats en production (230+ tests, 18 mois) : 89% via Tesseract (52ms), 11% via EasyOCR (310ms). Fiabilité : 99.3%. Avant double moteur : 94.1%.
4 Validation numérique
import re
class ValidateurNumerique:
def extraire_nombre(self, texte_ocr):
t = re.sub(r'[€$£¥\s]', '', texte_ocr.strip())
t = re.sub(r'[^\d.,]', '', t)
if ',' in t and '.' in t:
if t.index(',') < t.index('.'):
t = t.replace(',', '') # 1,234.56 → 1234.56
else:
t = t.replace('.', '').replace(',', '.') # 1.234,56 → 1234.56
elif ',' in t:
t = t.replace(',', '.')
return float(t)
def valider_prix(self, texte_ocr, attendu, tolerance=0.01):
lu = self.extraire_nombre(texte_ocr)
ok = abs(lu - attendu) <= tolerance
print(f"{'✅' if ok else '❌'} Lu: {lu} | Attendu: {attendu}")
return ok
v = ValidateurNumerique()
assert v.valider_prix("12.50 €", 12.50)
assert v.valider_prix("1 234,56 €", 1234.56)
assert v.valider_prix("EUR 12,50", 12.50)
✅ Ce prétraitement améliore le taux Tesseract de 60% à 87% sur des interfaces avec fonds colorés (rouge/vert, police LCD). Combiné au fallback EasyOCR : 99.3%.
1. Préparer les images de référence
Avant d'écrire une seule ligne de test, toutes les images nécessaires doivent être capturées et validées. C'est l'étape que les débutants sautent et qu'ils regrettent.
Protocole de capture — à suivre pour chaque image
Ouvrir l'application dans l'état exact où le test va la trouver
Lancer SikuliX IDE → bouton 📷 "Take screenshot"
Capturer uniquement l'élément — pas son fond, pas ses voisins
Nommer explicitement : btn_se_connecter.png, pas bouton1.png
Sauvegarder dans le bon sous-dossier de Pattern/
Valider avec le mini-script ci-dessous — score > 0.92 requis
// Mini-script de validation d'une image — colle dans SikuliX IDE et lance
Screen s = new Screen();
String img = "Pattern/Actions/btn_se_connecter.png";
Match m = s.exists(img, 3);
if (m != null) {
m.highlight(2);
System.out.println("✅ Valide — score : " + m.getScore());
// Score > 0.95 = excellent | 0.90-0.95 = bon | < 0.90 = recapturer
} else {
System.out.println("❌ Non trouvée — recapturer");
}
Liste des images pour un test de connexion
Pour un test de connexion simple, voilà ce qu'il faut capturer :
Fichier
Ce que c'est
Dossier
ecran_login.png
Logo ou titre de l'écran de login — identifie que l'écran est là
Login/
label_identifiant.png
Le label "Identifiant :" — permet de localiser le champ
Login/
label_mot_de_passe.png
Le label "Mot de passe :"
Login/
btn_se_connecter.png
Le bouton de connexion
Login/
icone_accueil.png
Un élément stable du tableau de bord — prouve que la connexion a réussi
Navigation/
message_erreur_login.png
Le message d'erreur si les identifiants sont mauvais
Login/
2. Créer les constantes
public class LoginImageConstants {
private static final String BASE = "Pattern/Login/";
public static final String ECRAN_LOGIN = BASE + "ecran_login.png";
public static final String LABEL_IDENTIFIANT = BASE + "label_identifiant.png";
public static final String LABEL_MDT_DE_PASSE = BASE + "label_mot_de_passe.png";
public static final String BTN_SE_CONNECTER = BASE + "btn_se_connecter.png";
public static final String MESSAGE_ERREUR = BASE + "message_erreur_login.png";
private LoginImageConstants() {}
}
💡 Les constantes ICONE_ACCUEIL et autres éléments communs sont déjà dans ImageConstants. Pas de doublon.
3. Instancier le framework
Selon l'environnement — local ou VNC distant — l'initialisation diffère. Le test ne change pas.
// ── Mode local ──
ScreenOperationsManager som = new ScreenOperationsManager();
// ── Mode VNC distant ──
ScreenOperationsManager som = new ScreenOperationsManager("192.168.1.100", 5900, "password");
// ── Via variable d'environnement (CI/CD) ──
// Si -Dtest.env=vnc → VNC automatiquement
// Si -Dtest.env=local → local automatiquement
ScreenOperationsManager som = ScreenOperationsManager.createFromEnv();
💡 Dans Katalon Studio, le ScreenOperationsManager est instancié une fois dans le TestListener et partagé entre tous les tests via une variable globale.
4. Écrire le test
Structure d'un test visual testing bien écrit — les mêmes phases qu'un test unitaire : Arrange / Act / Assert.
/**
* Test de connexion avec identifiants valides.
* Prérequis : application démarrée, écran de login visible.
*/
@Test
public void testConnexionValide() {
// ── ARRANGE — Vérifier les prérequis ──
// L'écran de login doit être affiché avant de commencer
boolean loginVisible = som.waitForElement(LoginImageConstants.ECRAN_LOGIN, 20);
if (!loginVisible) {
som.captureScreen(RunConfiguration.getReportFolder());
KeywordUtil.markFailedAndStop("Écran de login non trouvé — vérifier que l'application est démarrée");
return;
}
// ── ACT — Exécuter les actions ──
// Saisir l'identifiant (cliquer sur le champ via son label + saisir)
som.clickNearImage(LoginImageConstants.LABEL_IDENTIFIANT, 0.92, 200, "right");
som.typeText("admin");
// Saisir le mot de passe
som.clickNearImage(LoginImageConstants.LABEL_MDT_DE_PASSE, 0.92, 200, "right");
som.typeText("motdepasse123");
// Cliquer sur "Se connecter"
som.clickOn(LoginImageConstants.BTN_SE_CONNECTER);
// ── ASSERT — Vérifier le résultat ──
// Le tableau de bord doit apparaître dans les 10 secondes
boolean connexionReussie = som.waitForElement(ImageConstants.ICONE_ACCUEIL, 10);
if (!connexionReussie) {
// Vérifier si c'est un message d'erreur
boolean erreurPresente = som.waitForElement(LoginImageConstants.MESSAGE_ERREUR, 2, true);
som.captureScreen(RunConfiguration.getReportFolder());
String msg = erreurPresente
? "Connexion refusée — identifiants invalides"
: "Tableau de bord non trouvé après connexion";
KeywordUtil.markFailedAndStop(msg);
return;
}
KeywordUtil.logInfo("✅ Connexion réussie — tableau de bord affiché");
// Validation visuelle finale du scénario
som.validateFinalVisualState("Connexion_Standard", "Apres_Connexion_Reussie");
}
L'application n'est pas démarrée ou l'écran n'est pas celui attendu
Vérifier manuellement l'état de l'application avant le test
FindFailed sur un bouton
Image de mauvaise résolution ou seuil trop élevé
Logger le score, recapturer l'image, réajuster le seuil
Clic au mauvais endroit
Image trop générique, présente à plusieurs endroits
Ajouter une région ou utiliser un Pattern plus spécifique
OCR lit mal
Fond coloré ou police non standard
Activer le double moteur, prétraiter l'image
Test instable (passe parfois)
Timing — l'interface n'est pas prête quand SikuliX cherche
Augmenter les timeouts de wait(), ajouter waitVanish sur les spinners
Le screenshot d'échec est ton meilleur allié
Quand un test échoue, la première chose à faire est d'ouvrir le screenshot dans FailureScreenshots/. Il montre exactement ce que SikuliX voyait au moment de l'échec. 90% des bugs se diagnostiquent en 30 secondes avec cette image.
6. Test complet annoté — le template
Voilà le template de test à copier pour chaque nouveau cas. Il intègre toutes les bonnes pratiques.
/**
* NomDuScenario — description en une phrase de ce que le test valide.
*
* Prérequis :
* - [lister les prérequis manuels, ex: "application démarrée", "utilisateur admin créé"]
*
* Données de test :
* - [lister les données utilisées]
*/
@Test
public void testNomDuScenario() {
// ═══════════════════════════════════════════
// PHASE 1 — PRÉREQUIS
// ═══════════════════════════════════════════
// Toujours vérifier que l'état initial est celui attendu.
// Si ce n'est pas le cas, le test s'arrête proprement avec une capture.
if (!som.waitForElement(ImageConstants.ECRAN_ATTENDU, 20)) {
som.captureScreen(RunConfiguration.getReportFolder());
KeywordUtil.markFailedAndStop("Prérequis non satisfait : écran attendu non trouvé");
return;
}
// ═══════════════════════════════════════════
// PHASE 2 — ACTIONS
// ═══════════════════════════════════════════
// Chaque action est une ligne lisible.
// On ne met jamais de logique métier ici.
som.clickOn(ImageConstants.BTN_ACTION_1);
som.waitForElement(ImageConstants.CONFIRMATION_ACTION_1, 5);
som.clickNearImage(ImageConstants.LABEL_CHAMP_SAISIE, 0.92, 200, "right");
som.typeText("Valeur de test");
som.clickOn(ImageConstants.BTN_VALIDER);
// ═══════════════════════════════════════════
// PHASE 3 — ASSERTIONS
// ═══════════════════════════════════════════
// Vérifier le résultat attendu.
// Toujours prévoir le cas d'échec avec capture.
boolean resultat = som.waitForElement(ImageConstants.RESULTAT_ATTENDU, 10);
if (!resultat) {
som.captureScreen(RunConfiguration.getReportFolder());
KeywordUtil.markFailedAndStop("Résultat attendu non trouvé après l'action");
return;
}
// Validation OCR si une valeur textuelle doit être vérifiée
boolean textePresent = som.waitForText("Opération réussie", 5);
if (!textePresent) {
som.captureScreen(RunConfiguration.getReportFolder());
KeywordUtil.markFailedAndStop("Message de succès absent");
return;
}
// ═══════════════════════════════════════════
// PHASE 4 — VALIDATION FINALE
// ═══════════════════════════════════════════
// Comparaison visuelle de l'état final du scénario.
som.validateFinalVisualState("NomDuScenario", "Apres_Action_Finale");
KeywordUtil.logInfo("✅ Scénario NomDuScenario validé avec succès");
}