FlaUI, Erfahrung aus dem Projekt

Vorwort

Automatisiertes Testen von grafischen Oberflächen ist ein wichtiges Thema. Für jede GUI-Technologie gibt es mehrere Bibliotheken, die es sorgsam auszuwählen gilt, um zügig ein qualitativ hochwertiges und akkurates Ergebnis zu bekommen.  

Im Bereich Web-Technologien gibt es viele bekannte Frameworks wie Selenium, Playwright, Cypress und viele mehr. Aber auch im Bereich WPF oder Winforms gibt es passende Vertreter und ich möchte heute FlaUI vorstellen.

FlaUI ist eine .NET-Klassenbibliothek für automatisiertes Testen von Windows Apps, insbesondere der UI. Sie baut auf den hauseigenen Microsoft Bibliotheken für UI Automation auf.

Abbildung: Pyramide der Test-Automatisierung

Geschichte

Am 20. Dezember 2016 hat Roman Roemer die erste Fassung von FlaUI auf Github veröffentlicht. Unter der Version 0.6.1 wurden die ersten Schritte in Richtung Klassenbibliothek für das Testen von .NET Produkten gelegt. Seitdem wurde diese konsequent und mit großem Eifer weiterentwickelt, um die Bibliothek mit neuen und besseren Funktionen zu erweitern. Die neueste Version trägt die Versionsnummer 4.0.0 und bietet Features wie z.B. das Automatisieren von WPF und Windows Store App Produkten sowie das Tool FlaUI Inspect, welches die Struktur von .NET Produkten ausliest und wiedergibt.

Installation

Über GitHub oder NuGet kann FlaUI geladen und installiert werden. Zudem werde ich für diesen Artikel und das nachfolgende Beispiel noch weitere Plugins / Frameworks sowie Klassenbibliotheken verwenden wie:

  • C# von OmniSharp
  • C# Extensions von Jchannon
  • NuGet Package Manager von Jmrog
  • .NET Core Test Explorer von Jun Han
  • Die neueste Windows SDK
  • NUnit Framework

Beispiel

In diesem Beispiel werde ich mit mehreren unterschiedlichen Methoden eine typisches Windows-App, hier den Taskmanager, maximieren bzw. den Ausgangszustand wiederherstellen. Außerdem sollen unterschiedliche Elemente markiert werden.

Bei der Arbeit an diesem Artikel ist mir aufgefallen, dass Windows ein besonderes Verhalten aufweist: Wird ein Programm maximiert, ändert sich nicht nur der Name und andere Eigenschaften des Buttons, sondern auch die AutomationID. Das führte dazu, dass ich den Methodenaufrufen zwei verschiedene Übergabestrings für die AutomationID, „Maximize“ und „Restore“, mitgeben musste, die beide denselben Button ansprechen.

Code (C#)

Zuerst starten wir die gewünschte Anwendung und legen uns eine Instanz des Fensters für die weitere Verwendung an:

var app = Application.Launch(@"C:WindowsSystem32Taskmgr.exe");
var automation = new UIA2Automation();
var mainWin = app.GetMainWindow(automation);

Desweiteren benötigen wir noch die Helper Class ConditionFactory:

ConditionFactory cf = new ConditionFactory(new UIA2PropertyLibrary());

Diese Helper Class bietet uns die Möglichkeit, Objekte nach bestimmten Bedingungen zu suchen. Wie zum Beispiel, die Suche nach einem Objekt mit einer bestimmten ID.

In den folgenden Methoden wollen wir, wie oben erwähnt, das Programm maximieren und wieder den Ausgangszustand herstellen. Außerdem wollen wir Elemente hervorheben:

Für die erste Methode werden wir mit FindFirstDescendant und FindAllDescendant arbeiten. FindAllDescendant sucht sich alle Elemente, die unterhalb des Ausgangselements liegen.  FindFirstDescendant findet das erste Element unterhalb des Ausgangselements, das mit der übergebenen Suchbedingung übereinstimmt und mit DrawHighlight wird ein roter Rahmen um das Element gelegt.

        static void FindWithDescendant(Window window, string condition, string expected)
        {
            window.FindFirstDescendant(cf => cf.ByAutomationId(condition)).AsButton().Click();
            var element = window.FindFirstDescendant(cf =>
            cf.ByAutomationId("TmColumnHeader")).FindAllDescendants();            
            foreach(var Item in element)
            {
                if(Item.IsOffscreen != true)
                {
                    Item.DrawHighlight();
                }
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

In der zweiten Methode nutzen wir FindFirstChild und FindAllChildren. Beide funktionieren fast gleich wie Descendant, nur dass hier nicht alle Elemente gefunden werden, sondern nur die, die direkt unter dem Ausgangselement liegen.

        static void FindWithChild(Window window, string condition, string expected)
        {
            window.FindFirstChild(cf => cf.ByAutomationId("TitleBar")).FindFirstChild(cf =>
            cf.ByAutomationId(condition)).AsButton().Click();
            var element = window.FindAllChildren();
            foreach(var Item in element)
            {
                    Item.DrawHighlight();
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

Und für die dritte Methode nehmen wir FindFirstByXPath und FindAllByXPath. Hier müssen wir, wie der Name sagt, den Pfad angeben. Bei First sollte es der genaue Pfad zum gewünschten Element sein und bei FindAll werden alle Elemente gesucht, die in dem Pfad gefunden werden können. Solltet ihr ein Programm untersuchen, welches ihr nicht kennt, hilft es, FlaUI Inspect zu nutzen, welches Eigenschafen wie den Pfad anzeigen, aber auch andere Informationen über Elemente von Windows-Apps darstellen kann.

        static void FindWithXPath(Window window, string expected)
        {
            window.FindFirstByXPath("/TitleBar/Button[2]").AsButton().Click();
            var element = window.FindAllByXPath("//TitleBar/Button");
            foreach(var Item in element)
            {
                    Item.DrawHighlight();
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

Als letztes müssen wir nur noch die Methoden aufrufen und ihnen die gewünschten Werte übergeben. Zum einen das Fenster was wir uns am Anfang angelegt haben und zum anderen die AutomationID des Maximieren-Buttons, welche sich wie erwähnt ändert, sobald der Button betätigt wurde.

     FindWithDescendant(mainWin,"Maximize", "Restore");
       FindWithChild(mainWin,"Restore", "Maximize");
       FindWithXPath(mainWin,"Restore");

Im Code sieht das bei mir wie folgt aus:

Schwachpunkte

Ein Problem sind selbst gebaute Objekte z.B. hatten wir in einem Projekt mit selbsterstellten Polygonen Buttons angelegt. Diese konnten weder von FlaUI Inspect noch von FlaUI selbst gefunden werden, was die Nutzung dieser in unseren Autotests stark eingeschränkt hat. Für solche Objekte muss ein AutomationPeer (stellt Basisklasse bereit, die das Objekt für die UI-Automatisierung nutzbar macht) angelegt werden, damit diese gefunden werden können.

Fazit und Zusammenfassung

FlaUI unterstützt mit UIA2 Forms und Win 32 Anwendungen sowie mit UIA3 WPF und Windows Store Apps. Es ist einfach zu bedienen und übersichtlich, da es mit relativ wenig Grundfunktionen auskommt. Außerdem kann es jederzeit mit eigenen Methoden und Objekten erweitert werden.

Auch die Software-Entwickler:innen sind zufrieden, da sie keine extra Schnittstellen und damit keine potenziellen Fehlerquellen für die Testautomatisierung einbauen müssen. Gerade weil FlaUI uns die Möglichkeit gibt, direkt die Objekte des zu testenden Programmes abgreifen zu können, brauchen wir keine zusätzliche Arbeitszeit darauf zu verwenden, für das Testing größere und fehleranfällige Anpassungen an der bestehenden Programmstruktur vorzusehen und zu verwalten.

Andererseits muss man um jedes Objekt automatisiert ansprechen zu können, seine AutomationID im Testcode mindestens einmal hinterlegen, damit sie für den Test auch nutzbar ist. Dies bedeutet, dass man praktisch die ungefähre Struktur des zu testenden Programmes nachbilden muss, was gerade bei komplexeren Programmen aufwändig sein kann. Und um dabei die Übersichtlichkeit zu erhalten, sollte man diese geclustert in mehreren, namentlich Aussagekräftigen, Klassen hinterlegen.

Wir werden es auf alle Fälle weiterverwenden und auch unseren Kolleg:innen empfehlen.

Automatisiertes Layout-Testing von Websites mit Galen

Die Implementierung von Tests in Softwareprojekten ist ein wichtiger Bestandteil des Entwicklungsprozesses. Dank ihrer Eigenschaften lassen sie sich parallelisiert und auf immer gleiche Weise ausführen, ohne zusätzliche Aufwände zu verursachen. Dies erlaubt es, eine schnelle und kosteneffektive Aussage zur Qualität des Softwaresystems zu treffen, wodurch sich eine generell höhere Qualität des gesamten Softwaresystems ergibt.

Auch auf der visuellen Ebene sind Tests wichtig. So gibt es auf Kundenseite meist klare Anforderungen und Vorgaben an das Layout einer Anwendung oder Website sowie auch an deren Unterstützung für verschiedene Endgeräte, Browser und Auflösungen. Diese Anforderungen zu testen, ist manuell ein enormer Aufwand und auch die teilautomatisierte Umsetzung gestaltet sich meist schwierig, da die aufgenommenen Screenshots der Anwendung für die verschiedenen Geräte, Browser und Auflösungen manuell verglichen werden müssen.

Das Galen-Framework soll diese Lücke schließen, indem es dem Anwender erlaubt, seine Testanforderungen in eigenem Programmcode zu formulieren und so eine vollautomatisierte Testabdeckung für das Layout einer Anwendung umzusetzen.

Das Galen-Framework

Galen ist ein Framework zum automatisierten Testen des Layouts einer Website. Durch seine Kompatibilität zu Selenium Grid, lässt es sich in verschiedene Testlandschaften wie z. B. BrowserStack integrieren. Werden also mit BrowserStack verschiedene Tests zu unterschiedlichen Browsern, Devices und Auflösungen durchgeführt, können die Layout-Tests mit Galen parallel dazu durchlaufen werden.

Key-Features

Eine Übersicht der Key-Features zu Galen ist nachfolgend dargestellt:

  • Integration in Selenium Grid
    Eine Integration in andere Test-Tools wie BrowserStack oder Sauce Labs ist möglich
  • Responsive Design
    Das Design von Galen berücksichtigt stark die Bedeutung von Responsive Design und soll die Implementierung dieser Tests vereinfachen
  • Eine verständliche Sprache für Nichtanwender, Einsteiger und Profis
    Über die Galen Specs Language lassen sich komplexe Anforderungen an ein Layout stellen, die unterschiedliche Browser-Fenstergrößen einschließen

Human Readable and Advanced Syntax

Basic Syntax

Mit der Galen Specs Language können komplexe Layouts beschrieben werden. Dies betrifft neben angezeigten Controls auch die Definition verschiedener Bildschirmgrößen und Browser. Ein Vorteil der Sprache ist die einfache syntaktische Definition der Testanforderungen und die einfache Lesbarkeit für Menschen, die nicht mit dem Framework und seiner Syntax vertraut sind. » Galen Specs Language Guide

Galen Basic Syntax
Abbildung 1: Basic Syntax (Quelle: galenframework.com)

Fortgeschrittene Techniken

Für fortgeschrittene Anwender gibt es verschiedene Techniken, die bei der Optimierung der Spezifikation helfen können. So bietet das Framework unter anderem umfangreiche Funktionalitäten für die Erstellung visueller Tests wie Bildvergleiche und Farbschemaverifizierung. » Galen Specs Language Guide

Galen Advanced Syntax
Abbildung 2: Advanced Syntax (Quelle: galenframework.com)

Testen für Profis

Geübte Anwender haben außerdem die Möglichkeit, eigene und komplexere Ausdrücke zu formulieren, um so mehrere Testabfragen in einer einzigen Zeile zu formulieren. Auf diese Weise können klare Spezifikationen sowie gut wartbarer und zuverlässiger Testcode geschrieben werden. » Galen Extras

Galen Testcode
Abbildung 3: Test like a Pro (Quelle: galenframework.com)

Testdokumentation

Für die Dokumentation der Testergebnisse stellt das Framework drei Features bereit:

Error Reporting

  • Galen generiert einen HTML Testbericht
  • Dieser beinhaltet alle Testobjekte einer Seite
  • Beispiel

Screenshots

  • Bei fehlerhaften Tests markiert das Framework das betreffende Element
  • Dies vereinfacht die Fehlersuche
  • Beispiel

Image Comparison

  • Für die visuelle Kontrolle erstellt Galen Bildvergleiche
  • Nicht übereinstimmende Bereiche werden markiert
  • Beispiel

Unterstützung der Testdurchführung in verschiedenen Sprachen

Die Implementierung der Tests ermöglicht Galen in drei Sprachen. Die bereits bekannte Basic Syntax sowie mit JavaScript und Java.

Basic Syntax

Die Basic Syntax soll den schnellen, aber trotzdem mächtigen Einstieg ermöglichen. Mit ihr lassen sich relativ einfach verschiedene Browser wie Firefox, Chrome oder der Internet Explorer für die Testausführung auswählen oder auf Selenium Grid umstellen.

Für den Zugriff auf schwieriger zu erreichende Seiten, welche zum Beispiel durch Sicherheitsmechanismen geschützt sind, gibt es die Möglichkeit, eigenes JavaScript auf der Client-Seite zu implementieren. Durch die Implementierungen von eigenem JavaScript auf der Testseite kann die Website für die durchzuführenden Layout-Prüfungen vorbereitet werden. » Galen Test Suite Syntax

Galen Test Execution Basic Syntax
Abbildung 4: Test Execution Basic Syntax (Quelle: galenframework.com)

JavaScript

Durch die Verwendung von JavaScript steht es dem Anwender frei, sein eigenes Test-Framework zu entwickeln und so komplexe Sachverhalte abzubilden. Das Galen Framework bietet dabei die vier folgenden Funktionalitäten zur Implementierung von JavaScript-Tests.
» JavaScript Tests Guide

  • Die Implementierung von Behandlungen vor und nach den Testvorgängen
  • Das Filtern und die Neuordnung von Testsequenzen
  • Die Verwaltung benutzerdefinierter Data Provider
  • Die Parametrisierung von Tests durch Arrays oder Maps
Galen Test Execution JavaScript Tests
Abbildung 5: Test Execution JavaScript Tests (Quelle: galenframework.com)

Java

Die eigentliche Sprache, die Galen zugrunde liegt, ist Java. Aus diesem Grund versteht es sich dann natürlich auch, dass für Java eine API zur Verfügung steht und dass die Java Virtual Machine zur Ausführung der Tests installiert sein muss. Die Java-API kann über das Central Maven Repository in Maven-Projekte eingebunden werden. » Git Beispielprojekt

Galen Test Execution Java API
Abbildung 6: Test Execution Java API (Quelle: galenframework.com)

Fazit

Die Durchführung von Layout-Tests ist eine aufwendige Aufgabe, die mit zunehmender Anzahl an Tests viele Ressourcen in Softwareprojekten kosten kann. Mit dem Galen Framework existiert eine Lösung der automatischen Durchführung und Dokumentation von Layout-Tests, die zudem eine komfortable Integration in bestehende seleniumbasierte und andere Teststrategien bietet. Durch ihre einfache und menschenlesbare Syntax ist sie für nahezu alle Projektteilnehmer verständlich und unterstützt somit die rollenübergreifende Zusammenarbeit im Softwareprojekt.

Mocks in der Testumgebung (Teil 1)

Die Komplexität und Interaktion der Softwareanwendungen, die in Unternehmen eingesetzt wird, ist in den letzten Jahren stark gestiegen. Werden heute Releases ausgerollt, steht ein Teil der neuen Funktionen immer im Zusammenhang mit dem Datenaustausch zu anderen Anwendungen. Das merken wir auch im Bereich Softwaretest: Der Fokus der Tests hat sich von reinem Systemtest auf den Bereich der Integrationstests erweitert. Darum kommen auch Mocks zum Einsatz.

Wir arbeiten in einem Testcenter und führen für unsere Kunden den übergreifenden Integrationstest über seine komplexe Anwendungsinfrastruktur durch. Daher bauen wir die Testumgebungen passend zur Produktivumgebung auf. Also wozu brauchen wir dann Mocks, wenn die Integrationstestumgebung bereits alle Software-Komponenten besitzen?

Zum Teil liegt man mit dieser Denkweise schon richtig, allerdings werden auf den unterschiedlichen Staging-Ebenen beim Test verschiedene Fokusse gesetzt. Mocks werden im Systemtest eher weniger verwendet, da in diesem Bereich die Funktionen der Software getestet werden. Dafür werden Mocks häufiger in Integrationstest verwendet. Da es aber nicht immer möglich ist, den kompletten Nachrichtenfluss zu testen, wird als Alternative die Kommunikation an der Schnittstelle geprüft.

Werden im Integrationstest 3 Komponenten getestet, die im Nachrichtenfluss direkt hintereinanderliegen, kann bei Verwendung aller 3 Softwarekomponenten nicht hundertprozentig sichergestellt werden, dass die Prozedur ohne Probleme läuft. Es könnte sein, dass die jeweiligen Fehler der Komponenten den Nachrichtenfluss verfälschen und im Nachhinein wieder richtiggestellt wird, der Fehler quasi maskiert wird. Daher stellen wir Mocks an die jeweiligen Enden der Komponenten, um gezielte Ein- und Ausgaben zu bekommen.

Um die beschriebenen Probleme und Eigenheiten eines Mocks zu erklären, nutzen wir als zu testende Anwendung einen Rest-Services. Der Rest-Service sollte in der Lage sein, ein GET und ein POST–Befehl durchzuführen. Würde unsere Komponente nun nach personenspezifischen Daten beim Mock nachfragen mit einem GET-Befehl, könnten wir unseren Mock so konfigurieren, dass er standardisierte Antworten zurück gibt oder bei POST-Befehlen einfache Berechnungen durchführt.

Mit Microsoft Visual Studio kam man in C# schnell ein WebAPI-Projekt erstellen, welches die grundlegende Funktion eines Rest-Services besitzt. Die Methoden des Controllers müssen nur noch angepasst werden und man hat eine funktionierende Rest-API zur Verfügung.

Datei > Neu > Projekt > Visual C# > Web > Webanwendung

Wenn man sich die Controller der WebAPI anschaut, kann man sehen, dass bestimmte URLs, Funktionen innerhalb der API aufrufen. In diesem Beispiel wird eine kleine WebAPI verwendet, die als Helden-Datenbank genutzt wird.

Heldenliste

Die Route beschreibt den URL-Pfad um die HttpGet(GET-Befehl)- oder HttpPost(POST-Befehl) Funktion aufzurufen.

Beispiel: http:localhost/api/helden/add

Sobald man die Rest-API zum Laufen gebracht hat, ist es mit einem API-Tool(Postman) oder einem Browser möglich, unterschiedliche Rest-Befehle an die URL zu schicken. Wenn nun ein POST-Befehl an die Rest-API geschickt wird, nimmt der Service diesen an und erkennt anhand der URL, dass die Funktion AddHeldenDetails() aufgerufen werden soll. Die Funktion nimmt die mitgeschickten Daten auf und fügt sie ihrer Datenbank hinzu. Als Antwort liefert sie den Return-Wert der Funktion. In diesem Fall die Bestätigung über das Hinzufügen des gewünschten Heldes.

POST-Befehl:

POST /api/Helden/zwei HTTP/1.1
Host: localhost:53521
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: b169b96f-682d-165d-640f-dbcafe52789e
{ "Name":"Maria","Klasse": "Lord","Alter":68,"Level":57 }

Antwort:

Der Held mit dem Namen: Maria wurde hinzugefügt

Wir haben nun mit unserem POST-Befehl die Heldin Maria der Datenbank hinzugefügt. Nun können mit dem GET-Befehl die gespeicherten Helden abrufen werden. Hier ein Beispiel zum Format des GET-Befehls, welcher an die Rest-API geschickt wird mit der dazugehörigen Antwort der API:

Abfrage:

GET /api/helden/get HTTP/1.1
Host: localhost:53521
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: b3f19b01-11cf-85f1-100f-2cf175a990d9

Antwort:

Antwort der API

In der Antwort ist zusehen, dass die Heldin Maria der Liste hinzugefügt wurde und nun jederzeit abgerufen werden kann.

Nun wissen wir um die Funktionsweise den Rest-Services Bescheid, welcher Input zu welchem Output führt. Mit dieser Information können wir uns dann an den Bau eines Mocks begeben. Mit diesem Thema werde ich mich im nächsten Teil genauer befassen.

Das war „Mocks in der Testumgebung Teil 1“ … Teil 2 folgt 🙂