Tests UI automatisés pour applications de bureau Windows — le fonctionnement de UI Automation et la construction de tests robustes avec FlaUI

· Mis à jour le: · · Tests, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD

« À chaque publication, la vérification consistant à cliquer manuellement sur tous les écrans prend deux journées entières. » « Un autre écran, juste à côté de ce qu’on avait corrigé le mois dernier, s’est cassé, et c’est le client qui l’a découvert. » « L’équipe Web automatise ses tests de non-régression avec Selenium ou Playwright, mais l’application de bureau reste totalement intouchée. » Nous recevons souvent ce type de retour de la part d’équipes qui maintiennent depuis longtemps des applications métier WinForms/WPF.

À l’inverse, nous entendons tout autant d’histoires d’échec : une équipe se lance avec enthousiasme dans l’écriture de tests de fumée (smoke tests) pour tous les écrans, puis abandonne au bout d’un an, incapable de suivre la charge de maintenance. Si l’on se trompe sur le choix des outils et sur la limite de « jusqu’où aller », les tests UI automatisés finissent par coûter plus cher que les tests manuels. À l’inverse, en comprenant le mécanisme sous-jacent, en concevant des tests robustes, et en limitant le périmètre aux tests de fumée, cela devient un investissement qui transforme « deux jours avant chaque publication » en « vingt minutes sans surveillance chaque nuit ». Cet article passe en revue l’ensemble du modèle que nous utilisons sur nos projets réels : le fondement — le fonctionnement de UI Automation (UIA) —, l’état actuel des outils (FlaUI / WinAppDriver / Appium), l’implémentation avec FlaUI, la conception pour la robustesse, et l’exécution sans surveillance en CI.

1. L’essentiel d’abord

  • Les tests UI automatisés se situent tout en haut de la pyramide des tests. Lents, fragiles et longs à diagnostiquer en cas d’échec, ils ne remplacent pas les tests unitaires et d’intégration. Protégez la logique dans les couches inférieures, et limitez les tests UI à des tests de fumée vérifiant que « l’application démarre et que les parcours principaux fonctionnent » (chapitre 6).
  • Le fondement du mécanisme est Windows UI Automation (UIA). On recherche les éléments dans l’arborescence d’automatisation enracinée sur le bureau, on les identifie par des propriétés telles que AutomationId / Name, et on les opère via des modèles de contrôle (Invoke / Value / SelectionItem, etc.). 12
  • Avant d’écrire du code, vérifiez comment les éléments de l’application cible sont exposés avec inspect.exe ou Accessibility Insights for Windows. inspect est un outil hérité fourni avec le Windows SDK ; Accessibility Insights est désormais l’outil officiellement recommandé. 3
  • Notre recommandation d’outillage est FlaUI + xUnit / NUnit. FlaUI est une bibliothèque open source sous licence MIT qui encapsule finement UIA, prend en charge à la fois UIA2 et UIA3, et reste activement maintenue à ce jour. 4
  • Autrefois choix officiel de Microsoft, WinAppDriver en est resté à sa dernière version stable v1.2.1 de novembre 2020, et comme le code source du serveur lui-même n’est pas public, la communauté ne peut même pas le corriger. Nous ne recommandons pas de l’adopter pour un nouveau projet (chapitre 3). 5
  • 80 % de la robustesse dépend d’une convention côté développement pour assigner l’AutomationId. En WPF, c’est x:Name ou AutomationProperties.AutomationId ; en WinForms, c’est Name / AccessibleName. Interdisez les recherches basées sur le texte affiché (Name), les clics par coordonnées et Thread.Sleep, et écrivez vos tests avec des attentes conditionnelles (Retry) et le modèle Page Object (chapitres 4 à 5). 67
  • L’exécution sans surveillance en CI exige une session de bureau interactive. Cela ne fonctionne pas avec un agent configuré comme un service, et cela échoue aussi en cas de verrouillage de l’écran ou de déconnexion RDP. La configuration de base consiste en un exécuteur auto-hébergé configuré avec une connexion automatique (autologon) (chapitre 7). 8

2. Le mécanisme — arborescence, propriétés et modèles de UI Automation

2.1 L’arborescence d’automatisation

Les outils de tests UI automatisés ne reconnaissent pas l’écran comme une image. Windows dispose d’une plateforme officielle appelée UI Automation (UIA) qui permet aux technologies d’assistance telles que les lecteurs d’écran de lire et d’opérer par programme l’UI d’une application, et l’automatisation des tests circule sur ce même rail. 1

Dans UIA, toutes les fenêtres ouvertes et les contrôles qu’elles contiennent sont exposés sous forme d’une structure arborescente enracinée sur le bureau. Boutons, zones de texte, lignes de grille : tout est un élément d’automatisation dans l’arborescence. Outre la vue « raw » qui contient tous les éléments, l’arborescence propose des vues filtrées : la vue de contrôle, limitée aux contrôles réellement opérables, et la vue de contenu, limitée aux éléments porteurs de contenu. 1 Le code de test parcourt en général la vue de contrôle pour trouver les éléments.

Le point clé à retenir ici est que l’on ne peut opérer que ce qui est exposé dans l’arborescence, pas ce qui est visuellement visible. Les écrans construits avec des contrôles standard s’exposent proprement dans l’arborescence, mais une liste dessinée soi-même en owner-draw, une zone de dessin réalisée avec une bibliothèque graphique, ou une partie d’une grille tierce peuvent n’apparaître que comme « un seul élément » dans l’arborescence. Ce que l’arborescence expose fixe directement la limite supérieure de ce que les tests UI automatisés peuvent couvrir — c’est précisément pourquoi vérifier l’arborescence avant d’écrire du code (section 2.3) est la première chose à faire.

2.2 Propriétés et modèles de contrôle

Les propriétés sont les indices qui permettent d’identifier l’élément recherché dans l’arborescence. En pratique, quatre propriétés comptent :

Propriété Contenu Rôle dans les tests
AutomationId Identifiant attribué par le développeur, indépendant de la langue (locale) Clé de recherche de référence. Doit être unique parmi les éléments frères 6
Name Nom dérivé du texte affiché (par ex. le libellé d’un bouton) Facile à lire pour un humain, mais se casse lors d’un changement de texte ou d’une localisation
ControlType Type de contrôle — Button, Edit, ComboBox, etc. Aide au filtrage
ClassName Nom de la classe d’implémentation (par ex. un nom de classe WinForms) Dernier recours ; fragile face aux changements d’implémentation

AutomationId est officiellement défini comme devant « rester identique quelle que soit la locale » et « être unique parmi les éléments frères » : c’est une propriété dédiée qui permet aux tests UI automatisés de retrouver un élément de façon stable, quels que soient la langue ou la version. 6 Autrement dit, tester l’UI d’une application sans AutomationId oblige à s’appuyer sur des indices fragiles comme le texte affiché ou la position dans l’arborescence. C’est le sujet du chapitre 5.

Les modèles de contrôle (control patterns) sont le moyen d’opérer un élément une fois trouvé. UIA expose les capacités fonctionnelles d’un contrôle — « peut être cliqué », « possède une valeur », « peut être sélectionné » — comme un ensemble de modèles indépendants du type de contrôle. La documentation elle-même décrit la relation entre les modèles de contrôle et l’UI comme « analogue à la relation entre un objet COM et ses interfaces » : on interroge l’élément pour savoir quels modèles il implémente, puis on l’opère via ce modèle. 2 Pour qui connaît COM, on peut voir cela comme l’équivalent UI de QueryInterface. Voici les principaux modèles :

Modèle Action Contrôles typiques
Invoke Exécute l’action par défaut (équivalent d’un clic) Boutons, éléments de menu
Value Obtient/définit une valeur Zones de texte
SelectionItem / Selection Sélectionne des éléments, récupère l’état de sélection Listes, listes déroulantes, onglets
Toggle Bascule marche/arrêt Cases à cocher
ExpandCollapse Développe/réduit Listes déroulantes, éléments d’arborescence
Text Lit le contenu textuel Documents, texte enrichi
Window Maximise/réduit/ferme Fenêtres de premier niveau
Scroll / ScrollItem Fait défiler, amène un élément dans la zone visible Listes, grilles

Le code de test qui « clique sur un bouton » consiste en réalité, en interne, à « récupérer le modèle Invoke de cet élément et appeler Invoke() ». Plutôt que de calculer des coordonnées et d’envoyer des événements de souris, on appelle l’opération que le contrôle expose lui-même — cette différence est ce qui rend les tests stables, indépendamment de la position de la fenêtre ou du DPI.

2.3 Vérifier « ce qui est exposé » avec inspect.exe et Accessibility Insights

Le meilleur moyen de savoir avec quels AutomationId / Name / ControlType / modèles les éléments de l’application cible sont exposés est de les observer directement avec un outil.

  • inspect.exe : l’outil classique fourni avec le Windows SDK (se trouve sous bin\<version>\<platform> dans le répertoire d’installation du SDK). Sélectionnez un élément avec la souris ou le focus clavier pour afficher la liste de ses propriétés et modèles UIA, et vérifier aussi la navigation dans l’arborescence. Il est toutefois officiellement positionné comme un « outil hérité », et la migration vers Accessibility Insights est recommandée. 3
  • Accessibility Insights for Windows : l’outil actuellement recommandé par Microsoft. Sa fonction Live Inspect, qui permet de consulter les propriétés UIA d’un élément par simple survol de la souris ou déplacement du focus, est pratique, et elle inclut aussi des vérifications automatiques d’accessibilité (FastPass). 3
  • FlaUInspect : l’inspecteur fourni avec le projet FlaUI. Il permet de consulter l’arborescence selon les perspectives UIA2 et UIA3 réellement utilisées par FlaUI ; il est donc utile de l’installer aussi si vous écrivez des tests avec FlaUI. 4

D’expérience, la première chose à faire lorsqu’on introduit des tests UI automatisés n’est pas « écrire du code de test », mais ouvrir les écrans principaux avec un outil de type inspect et faire l’inventaire de la couverture d’AutomationId déjà en place. Si cet inventaire est trop clairsemé, il est finalement plus rapide de commencer par des corrections côté application (chapitre 5).

3. Le choix des outils — pourquoi recommander FlaUI, et où en est WinAppDriver

Il est possible d’appeler UIA directement via COM, mais en pratique on utilise une bibliothèque d’encapsulation. Voici où en sont les options en 2026, sur la base de faits vérifiés.

Outil Forme État (en 2026) Recommandation pour un nouveau projet
FlaUI Bibliothèque .NET (MIT) OSS activement maintenu. v5.0.0 publiée en février 2025 4 Premier choix
WinAppDriver Serveur protocole WebDriver (Microsoft) Dernière version stable v1.2.1 en novembre 2020. La v1.3 reste une RC de juillet 2020. Plus de 1 100 issues non résolues 5 Pratiquement à l’arrêt. À éviter pour un nouveau projet
Appium Windows Driver Pilote Windows d’Appium Utilise WinAppDriver en interne, il hérite donc des mêmes contraintes 9 Seulement si vous disposez déjà d’un patrimoine Appium
Coded UI Tests Fonctionnalité de Visual Studio Déprécié dans VS 2019, supprimé dans VS 2026 10 Cible de migration

3.1 FlaUI — actuellement l’encapsulation la plus pratique de UIA

FlaUI est une bibliothèque .NET qui facilite les tests UI automatisés des applications Windows (Win32 / WinForms / WPF / applications Store), conçue comme une encapsulation des bibliothèques UIA natives de Microsoft. 4 L’organisation des packages sépare la partie commune FlaUI.Core des packages FlaUI.UIA2 / FlaUI.UIA3, choisis selon l’implémentation UIA utilisée.

Le choix entre UIA2 et UIA3 est explicité dans la FAQ de FlaUI : UIA2 est une implémentation purement managée qui ne prend pas en charge les fonctionnalités récentes comme le tactile et fonctionne moins bien avec WPF ou les applications Store ; UIA3 est l’implémentation la plus récente, idéale pour WPF/Store, mais peut rencontrer dans les applications WinForms des bogues absents d’UIA2. 4 Autrement dit, le repère pratique est : essayez UIA3 pour WPF et UIA2 pour WinForms, et utilisez celui qui se révèle stable sur votre application réelle. Pouvoir installer les deux depuis NuGet et basculer de l’un à l’autre est l’un des avantages du fait que FlaUI reste une encapsulation légère.

FlaUI n’a pas son propre framework de test, on l’écrit donc en combinaison avec xUnit / NUnit / MSTest, comme un projet de test ordinaire. Le fait de tourner sur le même lanceur de tests et le même pipeline CI que les tests unitaires apporte un vrai bénéfice opérationnel.

3.2 WinAppDriver — pour être honnête, il est pratiquement à l’arrêt

WinAppDriver est un serveur de test UI produit par Microsoft, qui permet d’opérer des applications Windows via le même protocole WebDriver que Selenium ; il a un temps été considéré comme le choix évident. Lorsque les Coded UI Tests de Visual Studio ont été dépréciés, Microsoft lui-même recommandait comme voie de migration « Selenium pour le Web, Appium + WinAppDriver pour le bureau et UWP ». 10

Mais en consultant la situation actuelle sur GitHub, on constate que la dernière version stable, v1.2.1, est sortie en novembre 2020, sans aucune version stable publiée depuis. La v1.3 reste une Release Candidate (v1.2.99) de juillet 2020, jamais devenue version définitive, et il y a désormais plus de 1 100 issues non résolues. 5 Ce qui est encore plus gênant, c’est que le dépôt GitHub ne contient que de la documentation, des exemples et le suivi des issues : le code source du serveur lui-même n’a jamais été publié. Cela signifie que la communauté ne peut même pas corriger les bogues. Notre position est que « c’est officiel, donc c’est sûr » n’est pas une raison suffisante pour l’adopter fraîchement en 2026. Si vous disposez déjà d’un patrimoine de tests fonctionnant sur WinAppDriver, il n’est pas nécessaire de le jeter immédiatement, mais arrêtez d’en ajouter et orientez les nouveaux parcours de test vers FlaUI.

3.3 Appium Windows Driver et les autres options

Appium Windows Driver (appium-windows-driver) est le pilote permettant d’opérer des applications Windows depuis Appium, mais il s’agit en réalité d’une « interface vers WinAppDriver fourni par Microsoft », tout le traitement lourd étant assuré côté WinAppDriver. 9 Il hérite donc intégralement de la stagnation de WinAppDriver. Il reste une option pour les équipes déjà standardisées sur Appium pour le mobile et le Web, ou qui souhaitent écrire leur code de test en Java ou en Python — mais dans ce cas également, adoptez-le en connaissance de cause des contraintes et des perspectives limitées héritées de WinAppDriver.

Les applications WinUI 3 (Windows App SDK) prennent elles aussi en charge UIA et peuvent donc être testées avec FlaUI (UIA3). Cela dit, le retour d’expérience accumulé est plus mince que pour WinForms/WPF, ce qui rend la vérification préalable avec un outil de type inspect encore plus importante, en raison des particularités propres à chaque type de contrôle. Le choix du framework UI lui-même est traité dans « Comment choisir entre WinForms, WPF et WinUI — Tableau de décision pratique ».

4. Implémentation minimale avec FlaUI — lancement, recherche, opération, vérification

Assez de théorie : passons à du code qui fonctionne. Voici un projet de test ordinaire, avec FlaUI.UIA3 (ou FlaUI.UIA2 si WinForms se révèle instable) et xUnit installés depuis NuGet.

4.1 La forme de base d’un test de fumée

Un test pour le parcours « lancer l’application, ouvrir la boîte de dialogue de saisie de commande, enregistrer un enregistrement, et vérifier que le résultat apparaît dans le statut » peut s’écrire ainsi :

using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
using Xunit;

public class OrderSmokeTest
{
    [Fact]
    public void SaisieCommande_ParcoursPrincipalFonctionne()
    {
        using var app = Application.Launch(@"C:\App\OrderManager.exe");
        using var automation = new UIA3Automation();
        try
        {
            // Attend que la fenêtre principale apparaisse
            var window = app.GetMainWindow(automation);

            // Recherche l'élément par AutomationId et l'utilise comme Button
            window.FindFirstDescendant(cf => cf.ByAutomationId("NewOrderButton"))
                  ?.AsButton().Invoke();

            // La boîte de dialogue s'ouvre de manière asynchrone, on attend donc son apparition par une attente conditionnelle
            var dialog = Retry.WhileNull(
                () => window.FindFirstDescendant(
                          cf => cf.ByAutomationId("OrderDialog"))?.AsWindow(),
                timeout: TimeSpan.FromSeconds(5)).Result;
            Assert.NotNull(dialog);

            // Saisie via le modèle Value (pas une émulation clavier)
            dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox"))
                  .AsTextBox().Text = "Société Test";
            dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton"))
                  .AsButton().Invoke();

            // Vérifie aussi la fin de l'enregistrement via une attente conditionnelle sur le libellé de statut
            var saved = Retry.WhileFalse(
                () => window.FindFirstDescendant(
                          cf => cf.ByAutomationId("StatusLabel"))
                          ?.Name.Contains("Enregistré") == true,
                timeout: TimeSpan.FromSeconds(10));
            Assert.True(saved.Success);
        }
        finally
        {
            app.Close();
        }
    }
}

Il n’y a que quatre briques de base.

  • Lancement : Application.Launch démarre le processus (vous pouvez aussi vous connecter à une application déjà lancée avec Application.Attach). GetMainWindow attend que la fenêtre principale devienne disponible.
  • Recherche : FindFirstDescendant(cf => cf.ByAutomationId(...)) effectue la recherche dans l’arborescence. cf est une fabrique de conditions, et vous pouvez aussi composer des conditions ByName / ByControlType / And. Comme indiqué précédemment, la clé de recherche de référence reste AutomationId.
  • Opération : convertissez l’élément trouvé en wrapper typé tel que AsButton() / AsTextBox() / AsComboBox() pour l’opérer. Invoke() correspond au modèle Invoke, la propriété Text au modèle Value — les modèles de contrôle vus en section 2.2 se trouvent directement derrière ces appels.
  • Vérification : effectuez des assertions sur les résultats qui apparaissent dans l’UI (libellés, nombre de lignes d’une liste, titre de fenêtre, etc.). Si vous voulez vérifier le contenu de la base de données, rien n’empêche de la lire directement depuis le code de test.

4.2 Attendez avec Retry — écrire Sleep, c’est déjà perdu

La première source d’instabilité des tests UI (les fameux « flaky tests ») est le timing. Jusqu’à ce que la boîte de dialogue s’ouvre, jusqu’à ce que les données soient chargées, jusqu’à ce que le bouton devienne actif — l’UI change constamment de manière asynchrone, et si le code de test suppose que « c’est sûrement déjà affiché », on se retrouve avec un test qui échoue uniquement sur les machines lentes.

Cela ne veut pas dire qu’ajouter un Thread.Sleep(3000) est une bonne solution — c’est au contraire la pire. Sur une machine rapide, on attend inutilement ; sur une machine lente, ce n’est pas suffisant et le test échoue quand même ; et sur l’ensemble de la suite, le temps d’exécution total ne fait que s’accumuler. La réponse est l’attente conditionnelle : « interroger jusqu’à ce que la condition soit satisfaite, et abandonner au bout d’un délai » — et FlaUI fournit justement pour cela une classe Retry dédiée. 4

// Attend jusqu'à 5 secondes que ce ne soit plus null (c'est-à-dire que l'élément apparaisse)
var element = Retry.WhileNull(
    () => window.FindFirstDescendant(cf => cf.ByAutomationId("ResultGrid")),
    timeout: TimeSpan.FromSeconds(5),
    interval: TimeSpan.FromMilliseconds(200),
    throwOnTimeout: true).Result;

// Attend que la condition devienne vraie (c'est-à-dire que le bouton devienne actif)
Retry.WhileFalse(
    () => saveButton.IsEnabled,
    timeout: TimeSpan.FromSeconds(5),
    throwOnTimeout: true);

Retry.WhileNull / WhileFalse / WhileTrue / WhileException, entre autres, sont fournis, et vous pouvez y spécifier le délai d’attente, l’intervalle d’interrogation et si une exception doit être levée en cas de dépassement du délai. 4 Point d’attention : depuis la version 2.0, FlaUI a supprimé la nouvelle tentative implicite des méthodes de recherche (Find), adoptant la politique selon laquelle « c’est au code de test de décider explicitement où attendre ». 4 FindFirstDescendant ne regarde que « l’arborescence à cet instant précis » ; imposez donc en convention que toute recherche d’un élément apparaissant de façon asynchrone soit systématiquement enveloppée dans Retry. L’idée elle-même de « poser une condition d’attente plutôt que de s’en sortir avec Sleep » est d’ailleurs une règle de base de la programmation Windows en général, pas seulement des tests UI (voir « Pourquoi préférer une attente d’événement à Sleep(1) sous Windows »).

5. Concevoir pour la robustesse — conventions côté application et côté test

Les raisons pour lesquelles des tests UI finissent « abandonnés faute de pouvoir suivre la maintenance » se résument à peu près à quatre causes : les recherches basées sur le texte affiché, les clics par coordonnées, Sleep, et le manque de structure partagée. Éliminons chacune d’elles avec une convention.

5.1 Toujours assigner l’AutomationId côté développement

C’est le point le plus important. La robustesse des tests dépend, avant même la manière dont le code de test est écrit, du fait que l’application expose ou non des identifiants stables. AutomationId est précisément la propriété prévue pour cela : la spécification exige qu’elle soit indépendante de la locale et unique parmi les éléments frères. 6

En WPF, un élément auquel on a donné x:Name voit ce nom utilisé comme identifiant côté UIA ; un contrôle déjà nommé est donc testable sans travail supplémentaire. Pour en définir un explicitement — par exemple à l’intérieur d’un modèle de données — définissez la propriété attachée AutomationProperties.AutomationId. 67

<!-- x:Name devient directement l'identifiant -->
<Button x:Name="SaveButton" Content="Enregistrer" Click="OnSave" />

<!-- Définir explicitement AutomationProperties.AutomationId pour le contenu de modèle, par exemple -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
        Content="Supprimer"
        Command="{Binding DeleteCommand}" />

En WinForms, c’est Control.Name, le nom défini dans le concepteur (celui qu’on change de button1 à saveButton), qui sert à l’identification côté UIA. D’après notre expérience, les formulaires où Name a été assigné selon la convention peuvent presque toujours être recherchés tels quels par AutomationId, mais comme l’apparence varie selon la génération du framework et le type de contrôle, vérifiez toujours l’AutomationId réel avec un outil de type inspect avant de l’utiliser comme clé de recherche dans un test. Notez aussi que la propriété Name de UIA, qui devient le nom lu à voix haute par les lecteurs d’écran, est reprise de la propriété Text pour de nombreux contrôles, mais que pour des types qui ne la reprennent pas, comme TextBox ou ListView, il faut définir explicitement AccessibleName. 11 Le fait que la mise en place d’AutomationId ne serve pas uniquement aux tests, mais corresponde exactement au même travail que le support de l’accessibilité, est un bon argument en interne pour faire accepter un budget.

La convention peut rester simple ; nous faisons généralement ajouter ces deux lignes aux normes de codage de nos clients.

  • Tout contrôle placé sur un écran, dès lors qu’il peut potentiellement faire l’objet d’une opération ou d’une vérification, reçoit un Name (WinForms) / x:Name ou AutomationProperties.AutomationId (WPF) significatif
  • Une fois qu’un identifiant est référencé par un test, tout renommage doit se faire simultanément côté test (l’identifiant est considéré comme une API publique)

5.2 Interdire les clics par coordonnées

Une opération du type « cliquer sur les coordonnées écran (830, 412) » se casse dès que la position de la fenêtre, la résolution, la mise à l’échelle DPI, le thème ou les paramètres de police changent. Le DPI en particulier produit en masse des échecs du type « passe en local mais échoue en CI », à cause de différences d’environnement telles que 100 % sur la machine de développement contre 150 % sur la machine CI (le fonctionnement du DPI est décrit dans « Prise en charge du DPI élevé dans WinForms »). Comme on l’a vu au chapitre 2, opérer via les modèles de contrôle de UIA ne dépend pas des coordonnées. FlaUI dispose aussi d’API de manipulation directe de la souris, mais décidez à l’avance qu’elles ne sont utilisables que pour des opérations impossibles à exprimer via un modèle — le glisser-déposer ou un canevas de dessin, par exemple. Et même dans ce cas, calculez la position relative à partir du BoundingRectangle de l’élément plutôt qu’à partir des coordonnées écran.

5.3 Centraliser la structure en un seul endroit avec le modèle Page Object

Si vous inscrivez en dur du code de recherche comme FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) directement dans le corps des tests, vous vous retrouvez obligé de corriger tous les tests dès que la composition de l’écran change. La solution classique est le modèle Page Object : créer « une classe par écran (ou boîte de dialogue) » et y enfermer la recherche et l’opération des éléments.

public sealed class OrderDialogPage
{
    private readonly Window _dialog;
    public OrderDialogPage(Window dialog) => _dialog = dialog;

    // Le code de recherche d'éléments ne vit que dans cette classe
    private TextBox CustomerName =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")).AsTextBox();
    private Button Save =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton")).AsButton();
    private Label Status =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("StatusLabel")).AsLabel();

    // Les tests ne voient que des opérations exprimées en termes métier
    public void Register(string customerName)
    {
        CustomerName.Text = customerName;
        Save.Invoke();
        // Effectue aussi la recherche de l'élément et le test de nullité dans le retry. Sur les écrans
        // où le libellé de statut est créé/redessiné après l'enregistrement, il est normal
        // que la recherche renvoie brièvement null ou lève une exception (sans ignoreException,
        // la première exception ferait échouer l'appel immédiatement)
        Retry.WhileFalse(() => Status?.Name.Contains("Enregistré") == true,
            timeout: TimeSpan.FromSeconds(10), throwOnTimeout: true,
            ignoreException: true);
    }
}

Le corps du test peut désormais s’écrire en termes métier, new OrderDialogPage(dialog).Register("Société Test"), et l’impact d’un changement d’écran se limite à une seule correction dans le Page Object. Pour continuer à faire tourner des tests UI sur une application de plus de 10 écrans, ce modèle est de fait indispensable. Une seule mise en garde : pour tout élément référencé depuis une condition d’attente, conservez toujours le principe de le rechercher à nouveau à chaque fois via une propriété, comme Status ci-dessus. Si vous mettez en cache le résultat de la recherche dans un champ, vous finirez par retenir un ancien élément disparu au redessin et attendre jusqu’au délai d’expiration.

5.4 Indépendance des tests — ne pas faire circuler d’état

Rendez chaque test UI indépendant. Créer une dépendance d’ordre du type « le test 3 suppose les données créées par le test 2 » provoque des échecs en cascade et empêche aussi de réordonner les tests. Les principes sont les suivants :

  • Chaque test (ou classe de test) lance lui-même l’application et la ferme systématiquement à la fin (via finally ou une fixture IDisposable)
  • Les données préalables sont préparées par le test lui-même. Prévoir côté application une option de lancement permettant de substituer le fichier de configuration ou la base de données pour les tests simplifie considérablement les choses
  • N’oubliez pas le nettoyage en cas d’échec. Un processus mal fermé ou les restes d’une boîte de dialogue modale deviennent la cause de l’échec du test suivant (chapitre 7)

6. Jusqu’où protéger avec des tests UI — une limite centrée sur les tests de fumée

Une fois les outils en place, on est tenté de tester tous les écrans — mais c’est précisément là que se joue le point de bascule. Les tests UI sont des ordres de grandeur plus lents que les tests unitaires (de quelques secondes à quelques dizaines de secondes chacun), plus fragiles (ils doivent suivre chaque changement d’UI), et plus longs à diagnostiquer en cas d’échec (bogue de l’application, bogue du test, ou environnement ?). C’est cette structure de coûts qui explique pourquoi le sommet de la pyramide des tests est dessiné étroit — protéger avec un test UI ce que les couches inférieures peuvent déjà protéger est toujours une perte nette.

Sous forme de tableau de décision, cela donne :

Ce qu’on veut protéger Couche appropriée Raison
Calculs, conversions, règles métier Tests unitaires Rapide et stable. Les couvrir via l’UI est hors de question
Accès BD, E/S fichier, intégrations externes Tests d’intégration Utilisent le réel sans avoir besoin de l’UI (voir comment tracer la frontière)
ViewModel / logique de présentation Tests unitaires Avec MVVM, testable sans UI
L’application démarre, le parcours principal fonctionne, on peut enregistrer Test de fumée UI C’est le terrain de bataille principal des tests UI
Parcours critiques manqués auparavant par une vérification manuelle Test UI (régression) À ajouter uniquement là où un dommage réel s’est produit
Mise en page de l’écran, dégradation visuelle Inspection visuelle / comparaison de captures d’écran Écrire cela sous forme d’assertions est un enfer de maintenance — limitez le nombre
Crash, fuite de handles et autres conditions anormales Une autre fondation Hors du périmètre des tests UI (voir Application Verifier)

La façon de démarrer que nous recommandons est de commencer avec « une dizaine de tests de fumée ». « Ça démarre et l’écran principal apparaît », « les principales fiches de référence peuvent s’ouvrir », « un document représentatif peut être enregistré, recherché et prévisualisé pour impression », « aucune erreur ne s’affiche à la fermeture » — automatisez tels quels les dix parcours que vous vérifiiez systématiquement à la main avant chaque publication. À cette échelle, l’écriture prend une à deux semaines, l’exécution nocturne tient en 20 à 30 minutes, et la charge de maintenance reste réaliste. Une fois le bénéfice ressenti, ajoutez au coup par coup des tests de non-régression pour les bogues de régression ayant réellement causé un dommage. À l’inverse, un plan visant « tous les écrans, tous les champs » échoue presque toujours en cours de route.

Un autre investissement important consiste à extraire la logique de l’UI plutôt que d’ajouter davantage de tests UI. Un écran dont la logique métier est écrite directement dans les gestionnaires d’événements ne peut être protégé que par un test UI, mais en déplaçant cette logique vers un ViewModel ou une classe de service, elle devient protégeable par des tests unitaires, et le test UI n’a plus qu’à vérifier le « câblage ». D’expérience, si vous ressentez le besoin d’un grand nombre de tests UI, c’est souvent un problème de conception plutôt qu’un problème de test. Pour la manière de stratifier l’ensemble des tests, voir « Comment tracer la frontière entre tests unitaires et tests d’intégration », et pour la pratique des tests de couche inférieure, voir aussi « Exigences minimales d’un logger maison et checklist de tests d’intégration ».

7. Les pièges de la CI et de l’exécution sans surveillance — sans bureau, pas de test UI

Tant que l’on exécute les tests UI écrits sur sa propre machine de développeur, tout va bien. Les pièges se concentrent au moment de « l’exécution sans surveillance chaque nuit en CI », et contrairement aux tests UI Web (qui se déroulent entièrement dans un navigateur headless), la racine de tous les problèmes est que les tests d’applications de bureau exigent une véritable session de bureau interactive.

7.1 Une session interactive est indispensable — ça ne fonctionne pas avec un agent lancé comme un service

Les agents CI (agents Azure Pipelines, exécuteurs auto-hébergés de GitHub Actions, etc.) sont normalement maintenus résidents en tant que services Windows. Mais un service n’a pas de bureau utilisateur, donc on ne peut pas opérer les fenêtres d’une application lancée depuis un tel service. La documentation officielle d’Azure Pipelines précise elle-même qu’un agent exécutant des tests UI d’application de bureau doit être configuré comme un processus interactif avec la connexion automatique (autologon) activée, et non comme un service. 8 Elle indique également que les agents hébergés par Microsoft (les exécuteurs partagés fournis côté cloud) ne prennent pas en charge les tests à UI visible : seuls les tests en navigateur headless y fonctionnent. 8 Autrement dit, une machine auto-hébergée (physique ou VM) est de fait indispensable pour les tests UI d’une application de bureau.

La configuration autologon comporte un risque de sécurité officiellement noté : « toute personne ayant un accès physique à cette machine peut utiliser le compte connecté automatiquement ». 8 Le principe de base est donc d’utiliser un compte et une machine (VM) dédiés aux tests, sans y placer d’identifiants de production.

7.2 Verrouillage d’écran, déconnexion RDP, résolution — les façons classiques d’échouer

Même une fois la session interactive mise en place, des pièges subsistent. Voici un tableau des symptômes et des parades.

Piège Symptôme Parade
Agent lancé comme service Aucun élément trouvé, ou l’application ne démarre pas du tout Reconfigurer en processus interactif + autologon 8
Verrouillage d’écran / économiseur d’écran Les opérations de saisie n’atteignent pas l’application et échouent Désactiver l’économiseur d’écran dans la configuration autologon. Demander une exemption de GPO pour les stratégies qui déclenchent un verrouillage 8
Déconnexion RDP via le bouton « X » La session se verrouille à l’instant même de la déconnexion, et tous les tests suivants échouent Exécuter tscon <ID de session> /dest:console pour ramener la session à la console avant de se déconnecter 8
Différences d’environnement de résolution/DPI Des tests qui passent en local échouent uniquement en CI Fixer la résolution (Azure Pipelines dispose d’une tâche pour cela). Uniformiser la mise à l’échelle à 100 % 8
Exécution des tests en parallèle La concurrence sur la souris/le clavier/le focus les corrompt mutuellement Exécuter les tests UI un par un, en série, par machine. Paralléliser en ajoutant des machines (VM) plutôt
Restes d’un échec précédent Un processus ou une boîte de dialogue modale résiduelle bloque le prochain lancement Nettoyer les processus cibles avant de démarrer le test. Toujours exécuter le nettoyage dans un finally

Le piège RDP est particulièrement facile à rencontrer, une précision s’impose donc. Si vous vous connectez à la machine de test en Bureau à distance pour faire des réglages, puis fermez la fenêtre et vous déconnectez, cette session se retrouve verrouillée, et les tests UI suivants continuent d’échouer. Le contournement recommandé par la documentation officielle consiste à exécuter, avant la déconnexion, %windir%\System32\tscon.exe <ID> /dest:console depuis une invite de commandes administrateur pour renvoyer la session vers la console. 8 Assurez-vous de l’inscrire dans le manuel d’exploitation de la machine de test.

Le DPI et la résolution méritent aussi votre attention. Il n’est pas rare qu’une VM de CI reste à 1024×768 avec une mise à l’échelle par défaut, ce qui change la mise en page et peut produire des différences telles que « un bouton visible sur la machine de développement n’apparaît qu’après défilement ». Éliminer les clics par coordonnées absorbe la majeure partie du problème, mais il reste préférable de fixer l’environnement. Les points de vérification décrits dans « Prise en charge du DPI élevé dans WinForms », y compris l’état de prise en charge du DPI côté application, s’appliquent directement ici.

7.3 Conserver des preuves en cas d’échec — captures d’écran et journaux

Lorsqu’un test UI exécuté sans surveillance échoue, un journal qui se contente de dire « élément introuvable » ne permet pas de comprendre la cause. Prévoyez dès le départ l’enregistrement d’une capture d’écran en cas d’échec. FlaUI dispose de fonctionnalités pour capturer l’écran ou un élément ; appelez-les depuis le hook d’échec de votre framework de test, et enregistrez le résultat comme artefact CI.

// À appeler depuis un hook d'échec ou similaire. Sortie vers le répertoire d'artefacts CI.
FlaUI.Core.Capturing.Capture.Screen()
    .ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));

En plus des captures d’écran, prévoyez de pouvoir rapprocher par horodatage le journal de l’application elle-même (jusqu’où le traitement est allé) et le journal côté test (quelles opérations ont réussi) : cela accélère considérablement le diagnostic entre « bogue de l’application, bogue du test, ou environnement ». Pour les points d’attention lorsqu’on exécute les tests la nuit via le Planificateur de tâches (type de session, problème de fin en 0x1, etc.), voir « Quand une tâche du Planificateur de tâches ne s’exécute pas, ou se termine en 0x1 ».

8. Résumé

Les tests UI automatisés ne sont pas quelque chose qui « fonctionne dès qu’on installe un outil » : ils ne continuent à tourner que lorsque la compréhension du mécanisme, la coopération côté application, une délimitation claire du périmètre et une conception soignée de l’environnement d’exécution sont réunies. Voici les points essentiels, condensés :

  • Le fondement est UI Automation. Rechercher dans l’arborescence par AutomationId, opérer via les modèles de contrôle. Commencer par faire l’inventaire de l’apparence de l’application cible avec inspect / Accessibility Insights
  • L’outillage est FlaUI + xUnit / NUnit. WinAppDriver n’a plus eu de version stable depuis 2020 — à éviter pour un nouveau projet
  • La robustesse se construit par convention. Assigner l’AutomationId côté développement, interdire les clics par coordonnées, interdire Sleep au profit d’une attente conditionnelle avec Retry, et centraliser la structure avec Page Object
  • Le périmètre part d’une dizaine de tests de fumée. Reporter la logique vers les tests unitaires et d’intégration, et cantonner les tests UI à la vérification que « le parcours principal fonctionne »
  • La forme de base de la CI est une machine auto-hébergée + une session interactive + autologon. Éliminer par la procédure d’exploitation les pièges du verrouillage d’écran, de la déconnexion RDP et des différences de résolution, et toujours conserver une capture d’écran en cas d’échec

« Deux journées de vérification manuelle avant chaque publication » peuvent réellement être remplacées, à un coût raisonnable, par des tests UI automatisés correctement délimités. À l’inverse, si votre application actuelle ne dispose d’aucun AutomationId, ou si sa logique est écrite directement dans les événements d’écran, il est finalement plus rapide de commencer par de petites corrections côté application avant d’écrire le moindre test. Nous pouvons vous accompagner dès l’étude de l’état actuel de votre application, pour déterminer par où commencer et mettre en place un ensemble de tests de fumée.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC prend en charge l’introduction de tests UI automatisés dans les applications WinForms / WPF (étude de l’état actuel, mise en place de l’AutomationId, construction d’un ensemble complet de tests de fumée, conception de l’environnement CI), la migration de patrimoines de tests existants vers FlaUI, ainsi que la revue de conception de la stratégie de test globale.

Références

  1. Microsoft Learn, UI Automation Overview. Sur l’arborescence d’automatisation enracinée sur le bureau, les vues raw / de contrôle / de contenu, les propriétés des éléments et les modèles de contrôle, et le fait que les technologies d’assistance et l’automatisation des tests utilisent la même fondation.  2 3

  2. Microsoft Learn, UI Automation Control Patterns Overview. Sur la conception des modèles de contrôle (l’analogie avec les interfaces COM), des modèles tels qu’Invoke / Value / SelectionItem, et le fait qu’un seul contrôle puisse implémenter plusieurs modèles.  2

  3. Microsoft Learn, Accessibility tools - Inspect. Sur le fait qu’inspect.exe soit fourni avec le Windows SDK et permette de consulter les propriétés et modèles UIA, et qu’il soit positionné comme un outil hérité, Accessibility Insights étant recommandé.  2 3

  4. GitHub, FlaUI/FlaUI. Sur le fait qu’il s’agisse d’une encapsulation UIA prenant en charge Win32 / WinForms / WPF / applications Store, l’organisation des packages FlaUI.Core / UIA2 / UIA3, le choix entre UIA2 et UIA3 (FAQ), la licence MIT, la sortie de la v5.0.0 (février 2025) et la poursuite du développement, l’utilitaire Retry et la suppression de la nouvelle tentative implicite des méthodes Find depuis la version 2.0.  2 3 4 5 6 7 8

  5. GitHub, microsoft/WinAppDriver. Sur le fait que la dernière version stable v1.2.1 soit sortie en novembre 2020, que la v1.3 reste une Release Candidate de juillet 2020 jamais mise à jour depuis, que le nombre d’issues non résolues dépasse 1 100, et que le dépôt se concentre sur la documentation et les exemples sans que le code source du serveur lui-même ne soit public.  2 3

  6. Microsoft Learn, Use the AutomationID Property. Sur le fait qu’AutomationId soit un identifiant indépendant de la locale, qu’il doive être unique parmi les éléments frères, son usage comme clé de recherche dans les scripts de test, et le fait qu’en WPF, les contrôles sans ID (x:Name) ni x:Uid ne prennent pas en charge AutomationId.  2 3 4 5

  7. Microsoft Learn, AutomationProperties.AutomationId Attached Property. Sur la définition de la propriété attachée, dans l’espace de noms System.Windows.Automation, permettant de définir en WPF une chaîne identifiant un élément de façon unique.  2

  8. Microsoft Learn, UI testing considerations (Azure Pipelines). Sur le fait que les tests UI d’application de bureau nécessitent une configuration de l’agent en processus interactif avec autologon activé, que les agents hébergés par Microsoft ne prennent pas en charge les tests UI visibles, le verrouillage causé par une déconnexion RDP et son contournement via tscon, la tâche de configuration de la résolution d’écran, et la collecte de captures d’écran/vidéo en cas d’échec.  2 3 4 5 6 7 8 9

  9. GitHub, appium/appium-windows-driver. Sur le fait qu’Appium Windows Driver soit une interface vers WinAppDriver fourni par Microsoft, et que le README avertisse que le serveur WinAppDriver n’est pas maintenu depuis longtemps.  2

  10. Microsoft Learn, Use Coded UI tests to test your code. Sur le fait que les Coded UI Tests soient dépréciés, Visual Studio 2019 étant la dernière version avec prise en charge complète, et qu’Appium + WinAppDriver ait été recommandé comme voie de migration pour les applications de bureau / UWP (la suppression dans VS 2026 est documentée dans le même guide de migration Learn).  2

  11. Microsoft Learn, WinForms: Setting the accessible name on a control. Sur le fait que la propriété UIA Name des contrôles WinForms soit, pour de nombreux types, reprise de la propriété Text, et que des contrôles comme TextBox / ListBox nécessitent de définir explicitement AccessibleName. 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Que faut-il utiliser pour les tests UI automatisés d'applications WinForms/WPF ?
Notre recommandation est FlaUI associé à un framework de test ordinaire comme xUnit ou NUnit. FlaUI est une bibliothèque open source sous licence MIT qui encapsule finement Windows UI Automation, prend en charge à la fois UIA2 et UIA3 (globalement : essayez UIA3 pour WPF, UIA2 pour WinForms), et est activement maintenue, avec la v5.0.0 sortie en février 2025. WinAppDriver, autrefois choix officiel de Microsoft, n'a plus eu de version stable depuis la v1.2.1 de novembre 2020, compte plus de 1 100 issues non résolues, et son code source serveur n'a jamais été publié ; nous ne le recommandons donc pas pour un nouveau projet. Les Coded UI Tests ont été dépréciés dans VS 2019 et supprimés dans VS 2026.
Comment les tests UI automatisés trouvent-ils et cliquent-ils sur les contrôles sans utiliser les coordonnées écran ?
Ils s'appuient sur Windows UI Automation (UIA), la même plateforme que celle utilisée par les lecteurs d'écran. Chaque fenêtre et chaque contrôle sont exposés comme un élément dans une arborescence enracinée sur le bureau ; les tests recherchent les éléments par leurs propriétés — AutomationId étant la clé de recherche de référence, car elle est définie comme indépendante de la locale et unique parmi les éléments frères — et les opèrent via des modèles de contrôle tels qu'Invoke (clic), Value (définir un texte) et SelectionItem. Comme le test appelle l'opération que le contrôle lui-même expose plutôt que d'envoyer des événements de souris à des coordonnées, il reste stable quels que soient la position de la fenêtre ou le DPI. Avant d'écrire du code, inspectez l'application avec inspect.exe ou Accessibility Insights for Windows pour voir ce qui est réellement exposé.
Combien de tests UI faut-il écrire, et que doivent-ils couvrir ?
Commencez avec une dizaine de tests de fumée couvrant les principaux parcours que vous vérifiiez auparavant à la main avant chaque publication : l'application démarre et l'écran principal apparaît, les principaux écrans de référence s'ouvrent, un document représentatif peut être enregistré et recherché, et aucune erreur n'apparaît à la fermeture. Les tests UI se situent en haut de la pyramide des tests — ils sont lents, fragiles et longs à diagnostiquer — protégez donc plutôt les calculs, les règles métier et l'accès aux données avec des tests unitaires et d'intégration. Les plans visant tous les écrans et tous les champs échouent presque toujours sous le poids de la maintenance ; si vous avez besoin de nombreux tests UI, c'est souvent un problème de conception, et déplacer la logique vers des ViewModels ou des services la rend testable unitairement.
Pourquoi mes tests UI passent-ils en local mais échouent-ils en CI ?
Les tests UI de bureau exigent une véritable session de bureau interactive, contrairement aux tests en navigateur headless. Un agent CI exécuté comme un service Windows n'a pas de bureau utilisateur, donc les recherches d'éléments échouent d'emblée — l'agent doit être configuré comme un processus interactif avec connexion automatique (autologon) sur une machine auto-hébergée, car les agents hébergés par Microsoft ne prennent pas du tout en charge les tests à UI visible. D'autres causes classiques sont le verrouillage d'écran et l'économiseur d'écran, la déconnexion RDP via le bouton X (qui verrouille la session — utilisez tscon pour la ramener à la console à la place), et les différences de résolution ou de DPI entre la machine de développement et la VM de CI. Enregistrez toujours une capture d'écran en cas d'échec pour pouvoir distinguer les bogues de l'application, les bogues du test et les problèmes d'environnement.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog