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.

Pimp my testAUTOmation (Teil 1)

Selenium 4 & Screenshots

Softwareentwicklungsprojekte leben vom Einsatz moderner Testwerkzeuge, welche die Projektbeteiligten bei ihrer Arbeit unterstützen. Selenium gibt es seit dem Jahr 2004 und wirkt ggf. etwas angestaubt, aber trotzdem ist es nicht aus der Mode. Mit Selenium 4 holt es zu den neuen Herausfordern auf. Mit dieser Blogreihe möchte ich zeigen, was Selenium 4 bringt und wie sich mit einfachen Mitteln wichtige Funktionen einbauen lassen wie z. B. Screenshots, Videos, Reports und Ansätze von KI. Dabei versuche ich, die Ansätze nach ihrem Mehrwert (The Good) und ihren Herausforderungen (The Bad) zu bewerten sowie ggf. nützliche Hinweise (… and the Useful) zu geben.

Jason Huggins begann bereits 2004 mit der Arbeit an Selenium als internem Projekt zum Testen von Webseiten. Mit der Zeit entwickelte sich Selenium zum führenden Werkzeug in vielen Entwicklungsprojekten oder diente als Grundlage für andere Testwerkzeuge. Aktuell fühlt sich das Framework schon etwas altbacken an, aber es besticht gegenüber seinen Herausforderern mit einer breiten Unterstützung von Sprachen (Ruby, Java, Python, C#, JavaScript) und Browsern (Firefox, Internet Explorer, Safari, Opera, Chrome, Edge u. a.).

Was ist neu in Selenium 4?

Die Version 4, die für 2020 angekündigt ist, versucht, Selenium nun in die Moderne zu holen. Dazu gehören folgende Neuerungen:

WebDriver API wird W3C StandardDamit wird es nur noch einen WebDriver für alle Browser geben.
Selenium4 IDE TNG„TheNextGeneration“ Selenium IDE basiert auf Node JS und steht neben Firefox auch für Chrome bereit. Es lassen sich parallele Testläufe starten und es gibt erweiterte Testprotokollinformationen (Testergebnis, Laufzeit etc.).
Verbessertes WebDriver GridDas Einrichten sowie die Administration und der Docker-Support wurden verbessert.
AußerdemEs gibt eine bessere UI und das Reporting / Logging wurden optimiert.
DokumentationMit Version 4 soll es eine ausführliche Dokumentation und neue Tutorials geben.

Mit Version 4 setzt sich Selenium aus folgenden Teilen zusammen: dem Selenium WebDriver, der Selenium IDE und dem Selenium Grid. Der Selenium WebDriver ist eine Sammlung von verschiedenen Programmiersprachintegrationen, um Browser für eine Testautomatisierung anzusteuern. Die Selenium IDE ist ein Chrome oder Firefox Add-on, um direkt aus dem Browser mit der Testautomatisierung ohne Programmierkenntnisse zu starten und ermöglicht die Aufnahme und das Abspielen von Testfällen im Browser. Das Selenium Grid ermöglicht die gesteuerte und gleichzeitige Testdurchführung auf verschiedenen Maschinen und unterstützt die Administration unterschiedlicher Testumgebungen von einem zentralen Punkt aus. Damit lässt sich ein Testfall gegen verschiedene Browser- bzw. Betriebssystem-Kombinationen testen oder es lässt sich eine Liste von Testfällen skaliert auf mehreren Maschinen verteilt durchführen.

Selenium 4

The GoodThe Bad… and the Useful
WebDriver API → W3C Standardized
Selenium 4 IDE TNG
Improved WebDriver Grid
Documentation
New challenger like cypress etc.
Selenium 4 was announced for 2019

Latest Selenium 4 Alpha version 4.0.0-alpha-5


Screenshots können beim Testen helfen!

Neuere Frameworks zur Testautomatisierung besitzen bereits eine Funktion zur Erzeugung von Screenshots. Doch mit ein paar Codezeilen lässt sich auch in Seleniumtests die Möglichkeit für die Ablage von Screenshots einbauen.

private void screenShot(RemoteWebDriver driver, String folder, String filename) {
 
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
    String timestamp  = dateFormat.format(new Date());
 
    try {
        File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
        // Now you can do whatever you need to do with it, for example copy somewhere
        FileUtils.copyFile(scrFile, new File(folder + filename + "_" + timestamp + ".png"));
    }
    catch (IOException e) {
        System.out.println(e.getMessage());
    }
     
}

Dabei sollte man aber immer bei der Erzeugung und der Ablage der Datei auf den Einsatzzweck von Screenshots achten. Screenshots können zum einen dem Debugging dienen und auf Probleme hinweisen. Darum ist es sinnvoll, ggf. die Screenshots nur dann zu erzeugen, wenn Probleme auftreten. Nach diesem Ansatz kann man die Screenshot-Funktionalität um einen flexiblen und globalen Schalter erweitern, der sich je nach Notwendigkeit setzen lässt.

Zum anderen können Screenshots der Dokumentation der Testergebnisse dienen und in manchen Projekten sogar vorgeschrieben sein, da hier gesetzliche oder andere Vorgaben eingehalten werden müssen. Dann muss die Ablage der Screenshots nachvollziehbar sein und sich jede erzeugte Datei einen Testfall und dem zugehörigen Testlauf zuordnen lassen. Nach diesem Ansatz muss der Dateiname einen Verweis auf den Testfall und einen passenden Zeitstempel haben. Darüber hinaus muss auch das Ablageverzeichnis für diesen einen Testlauf erzeugt und benannt werden.

Screenshots

The GoodThe Bad… and the Useful
Ermöglicht den Nachweis des Ergebnisses des TestlaufsKann nur einen Moment darstellenKann auch zum „Debuggen“ genutzt werden

Im meinem nächsten Beitrag erstellen wir ein Video der Testdurchführung.

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.

Kochrezepte für Testautomatisierung (Teil 3) – Wie muss ein richtiges (Test-) Rezept aussehen?

In den beiden vorhergehenden Themen „Zutaten und Küchengeräte für Testomaten und wer ist der Koch“ sowie „Testomaten auf Datensalat an Stressing“ habe ich darüber berichtet, welche Voraussetzungen für die Testautomatisierung gegeben sein müssen und welche Herausforderungen bei den Testdaten gemeistert werden müssen, um erfolgreich Automatisierung einzusetzen. Doch nun stellt sich die Frage, wie denn das Rezept, also ein Testfall für die Testautomatisierung, aussehen muss.

Dazu betrachten wir erst einmal ein typisches Kochrezept. Es besteht im Wesentlichen aus zwei Abschnitten, der Aufzählung der Zutaten (Testdaten) und der Beschreibung, in welcher Reihenfolge die Zutaten verarbeitet werden müssen. Die Beschreibung enthält dann sowohl die Schritte, die zur Zubereitung des Rezepts notwendig sind als auch die Erwähnung der Zutaten aus der Zutatenaufzählung. Nun haben auch Kochrezepte eine unterschiedliche Detailtiefe, je nachdem für wen die Rezepte bestimmt sind. Für den gelernten Chefkoch sind die Rezepte oft weniger detailliert, da der Koch bereits gewisse Arbeitsabläufe kennt und diese nicht näher beschrieben werden müssen. Rezepte für den Privathaushalt oder gar für einen Kochanfänger müssen da schon anders aussehen. So ist das auch bei den Testfällen. Für den Tester mit entsprechendem Domainwissen über die Fachlichkeit seiner Anwendung können die Testfälle weniger detailliert ausfallen. Wie ist das aber für einen Automaten? Dazu vergleichen wir hier einen Bäcker mit einem Brotbackautomaten. Für den Bäcker reicht das Rezept: Backe mir ein Roggenbrot. Für den Backautomaten ist eine genaue Rezeptbeschreibung notwendig – die Reihenfolge, wie die Zutaten in den Automaten gefüllt, welches Programm und welche Temperatureingestellt werden müssen usw.

Da wir in der Qualitätssicherung nicht nur ein Rezept bzw. einen Testfall haben, möchten wir uns jedoch die Arbeit etwas vereinfachen. So wie in der Großküche werden auch wir Vorbereitungen treffen, die uns später die Arbeit erleichtern. Während in der Küche z.B. die Salatbeilage für mehrere Gerichte verwendet wird, werden auch in der Testfallerstellung wiederverwendbare Testfallblöcke erstellt. Dazu werden mehrere Testschritte zusammengefasst und als Testschrittblock zur Wiederverwendung gespeichert. Dieses Verfahren kann sowohl beim manuellen Testen als auch in der Testautomatisierung angewendet werden. Der Unterschied liegt hier jedoch wieder in der Detailtiefe, d.h. dort, wo für das manuelle Testen ggf. eine geringe Detailtiefe ausreichend ist, wird für den Automaten immer die maximale Detailtiefe benötigt.

Teig wird per Hand geknetet, Backzutaten und Backutensilien liegen daneben
Abbildung 2: Brot backen

vs.

Ausschnitt von Code für Testfallerstellung
Abbildung 3: Testfallerstellung

Der Testautomat ist ja so gesehen eigentlich der schlechteste Koch der Welt. Er würde auch das Wasser anbrennen lassen, wenn wir ihm nicht sagen würden, dass der Topf vom Herd muss, wenn das Wasser blubbert. Aber warum benutzen wir dann überhaupt Testautomatisierung? Nun, der Testautomat hat einige wesentliche Vorteile: Ein Koch kann auch mal eine Zutat vergessen oder bei der Abarbeitung des Rezeptes variieren. Das Ergebnis ist dann nicht jedes Mal das gleiche Gericht. Der Automat vergisst hier nichts und hält sich immer an die vorgegebene Reihenfolge des Rezepts. Doch der größte Vorteil des Testautomaten ist die Geschwindigkeit, mit der er die Testfälle durchführen kann. Der Koch benötigt außerdem auch mal eine Pause. Würden wir uns so einen Automaten in der Küche vorstellen, bekämen wir sinnbildlich eine Gulaschkanone, die im Sekundentakt alle möglichen Rezepte abarbeitet und dessen Ergebnis auf den Teller schießt.

Das klingt für die Testautomatisierung sehr verlockend, dabei müssen jedoch immer wieder der Aufwand und der Nutzen ins Verhältnis gesetzt werden. Der Aufwand, so einen Automaten mit den perfekt designten Testfällen (Rezepten) zu füttern, wird oft unterschätzt: Wenn ich nur einmal im Jahr eine Geburtstagsfeier mit zehn Gästen habe, lohnt sich ein Kochautomat sicher nicht. Habe ich dagegen ein Eventunternehmen, welches jeden Tag eine ganze Hochzeitsgesellschaft à la Carte versorgen muss, ist so ein Automat definitiv eine Überlegung wert.

Protractor – Automatisiert Testen mit Angular

Kritische Fehler, die erst im Rahmen des Live-Betriebes öffentlich werden, stellen ein großes finanzielles Risiko und nicht zuletzt eine negative Werbung für ein Produkt und die beteiligten Unternehmen dar. Deshalb ist das Thema Test in der modernen Softwareentwicklung ein grundlegender und integraler Bestandteil. Durch eine hohe Testabdeckung und der zeitnahen Rückmeldung der Testergebnisse lässt sich die Qualität und Reife des Produktes ausreichend genau nachweisen und bestätigen.

Eine Lösung, die eine schnelle Durchführung dieser Tests ermöglicht und den Anforderungen moderner Entwicklungsprojekte entspricht, ist der Einsatz von Testautomatisierungswerkzeugen. Diese Werkzeuge arbeiten nach dem Prinzip der toolgesteuerten Aufnahme von Informationen über die grafische Oberfläche des Testobjekts und der damit möglichen automatisierten Durchführung von skriptgebundenen Interaktionen sowie der daraus resultierenden Prüfung der jeweiligen Applikation.

Testautomatisierungswerkzeuge sorgen für eine schnelle und kontinuierliche Rückmeldung über den Stand der Qualität der zu testenden Software. Aber bei ihrem Einsatz müssen einige Punkte beachtet werden. Es gibt verschiedene Werkzeuge auf dem Markt die unterschiedliche Ansätze wählen, wie sie sich in den Entwicklungs- und Testprozess integrieren oder welche Technologien sie unterstützen. Der effektive Einsatz einer Testautomatisierungslösung steht und fällt mit der verwendeten Engine, die zur Ansteuerung der grafischen Oberfläche genutzt wird. Diese muss die zu testende Technologie optimal unterstützen. Besonders Entwicklungsprojekte, die “neue” Technologien wie Angular2 einsetzen, haben die Herausforderung, dass die vorhandenen und bekannten Werkzeuge nicht immer auf dem gleichen Stand sind wie ihr Arbeitsgegenstand.

Projekt CLINTR und Tests mit Protractor

In unserem aktuellen Softwareentwicklungsprojekt Clintr nutzen wir Angular2 als Entwicklungsframework und wollten von Beginn an eine hohe Dichte von automatisierten Testfällen. Clintr ist eine Web-Anwendung, die Dienstleister auf potenzielle Kunden in ihrem Kontaktnetzwerk aufmerksam macht. Dazu werden Daten der angebotenen XING-API verwendet und analysiert, um nach bestimmten Kriterien vollautomatisiert einen Dienstleistungsbedarf bei Firmen abzuleiten. Wurde ein Dienstleistungsbedarf einer Firma identifiziert, sucht Clintr im Kontaktnetzwerk (z.B. XING oder CRM-Systeme) des Dienstleisters nach Kontaktpfaden zum potenziellen Kunden. Im Backend kommen Spring Boot basierte Microservices mit Kubernetes als Container Cluster Manager zum Einsatz, während im Frontend Angular (>2) eingesetzt wird. Um hochfrequent neue Versionen der Anwendung veröffentlichen zu können, wurde eine Continuous Delivery Pipeline in die Google-Cloud etabliert und das für Test- bzw. Produktionsumgebung.

Wir haben uns durch den Einsatz von Angular2 für das Automatisierungs-Testwerkzeug Protractor entschieden. Protractor baut auf Selenium und dem WebDriver Framework auf. Wie gewohnt laufen die Oberflächentests im Browser ab und simulieren das Verhalten eines Nutzers, der die Anwendung verwendet. Da Protractor direkt für Angular geschrieben wurde, kann es auf alle Angular-Elemente ohne Einschränkungen zugreifen. Darüber hinaus werden keine zusätzlichen Anweisungen für das Warten auf Komponenten wie “sleeps“ oder “waits“ benötigt, da Protractor selbst erkennt, in welchem Status die Komponenten sich befinden bzw. ob sie für die anstehende Interaktion zur Verfügung stehen.

How To

Für die Inbetriebnahme benötigt man AngularCLI und NodeJS. Danach können im Projekt die Oberflächentests (end-to-end oder e2e) erstellt werden. Zur Vorbereitung der lokalen Testausführung wechselt man mit der Konsole in das Projekt-Verzeichnis und gibt “ng serve” ein. Nach der Eingabe von “ng e2e” werden die Testfälle dann auf dem localhost ausgeführt.

Die end-to-end Tests bestehen aus Type Script Dateien mit der Endung .e2e-spec.ts, .po.ts oder nur .ts. In den Dateien, die mit .e2e-spec.ts enden, werden die Testfälle beschrieben. Nur Tests die in diesen Dateien stehen werden ausgeführt. In dem folgenden Beispiel sieht man den Kopf einer .e2e-spec.ts-Datei:

    import { browser, by, ElementFinder } from 'protractor';
    import { ResultPage } from './result-list.po';
    import { CommonTabActions } from './common-tab-actions';
    import { SearchPage } from './search.po';
    import { AppPage } from './app.po';
    import { CardPageObject } from './card.po';
    import * as webdriver from 'selenium-webdriver';
    import ModulePromise = webdriver.promise;
    import Promise = webdriver.promise.Promise;
     
    describe('Result list', function () {
     
     let app: AppPage;
     let result: ResultPage;
     let common: CommonTabActions;
     let search: SearchPage;
     
     beforeEach(() => {
     app = new AppPage();
     result = new ResultPage();
     common = new CommonTabActions();
     search = new SearchPage();
     result.navigateTo();
     });

Diese wird wie auch die anderen Dateitypen mit den Importen eröffnet. Darauf folgt der Beginn der Testfälle mit describe. In dem String in der Klammer wird angeben, welcher Bereich getestet werden soll. Darunter werden die einzelnen .po.ts Dateien angelegt und instanziiert, die für die darauffolgenden Tests benötigt werden. Durch die beforeEach Anweisung lassen sich Vorbedingungen für den Test definieren. Zum Zweck der Wiederverwendbarkeit lassen sich die Tests auch in Module auslagern (siehe nachfolgendes Code-Beispiel):

    it('should display the correct background-image when accessing the page', require('./background'));
    it('should send me to the impressum page', require('./impressum'));
    it('should send me to the privacy-policy page', require('./privacy-policy'));
     
    it('should open the search page after clicking clintr logo', require('./logo'));

Im nachfolgenden Code sind normale e2e Tests aufgeführt. Dort steht zuerst, was erwartet wird und danach die Ausführung des Tests. Hierbei sollte man sich merken, dass die e2e Tests in der .e2e-spec.ts nur die Methoden der .po.ts aufruft, und dann das Ergebnis zurückerwartet. Die ausführenden Methoden gehören in die .po.ts.

    it('should still show the elements of the searchbar', () => {
     expect(result.isSearchFieldDisplayed()).toBe(true);
     expect(result.isSearchButtonDisplayed()).toBe(true);
    });
     
    it('should show the correct Search Term', () => {
     expect(result.getSearchTerm()).toBe(result.searchTerm);
    });

Das nachfolgende Code-Beispiel zeigt die zu der vorherigen .e2e-spec.ts gehörigen .po.ts. Es ist nicht zwingend notwendig, dass jede .e2e-spec.ts ihre eigene .po.ts hat oder umgekehrt. Zum Beispiel kann eine .po.ts Aktionen von Tabs enthalten, wie Tab wechseln oder schließen. Solange eine .e2e-spec.ts nur Methoden von anderen .po.ts benutzt, benötigt sie nicht zwingend eine eigene .po.ts. Wie vorher erwähnt, beginnt die .po.ts mit den Importen und danach wird Klasse (im Beispiel ResultPage) erstellt.

Die navigateTo Methode lässt den Test bei ihrem Aufruf auf die vorgesehene Seite springen. Da der Test das in diesem Fall nicht direkt machen soll, geht er zuerst auf die Search Seite. Dort wird ein Suchbegriff eingegeben und die Suche gestartet. Somit kommt der Test auf die result_list Seite, wo dann die Tests ausgeführt werden.

    import { element, by, ElementFinder, browser } from 'protractor';
    import { SearchPage } from './search.po';
    import * as webdriver from 'selenium-webdriver';
    import { CardPageObject } from './card.po';
    import ModulePromise = webdriver.promise;
    import Promise = webdriver.promise.Promise;
     
    export class ResultPage {
     
     public searchTerm: string = 'test';
     
     search: SearchPage;
     
     navigateTo(): Promise<void> {
     this.search = new SearchPage();
     return this.search.navigateTo()
     .then(() => this.search.setTextInSearchField(this.searchTerm))
     .then(() => this.search.clickSearchButton());
     }

In den drei nachfolgenden Methoden wird jeweils ein Element der Seite abgefragt. Die ersten zwei Tests haben als Rückgabewert einen Union Type. Das heißt, dass entweder ein boolean oder ein Promise<boolean> zurückgegeben werden kann. Also entweder ein Boolean oder das Versprechen auf einen Boolean. Wenn man mit dem Rückgabewert Promise arbeitet, sollte darauf immer ein then folgen, da es sonst zu asynchronen Fehlern kommen kann.

    isSearchButtonDisplayed(): Promise<boolean> | boolean {
     return element(by.name('searchInputField')).isDisplayed();
    }
     
    isSearchFieldDisplayed(): Promise<boolean> | boolean {
     return element(by.name('searchButton')).isDisplayed();
    }
     
    getSearchTerm(): Promise<string> {
     return element(by.name('searchInputField')).getAttribute('value');
    }

Beispiel

Ein Umsetzungsbeispiel für einen Testfall in ClintR ist der Test des Impressumlinks. Er soll zuerst den Link drücken. Danach soll der Test auf den neu entstandenen Tab wechseln und bestätigen, ob die URL /legal-notice enthält. Als letztes soll er diesen Tab wieder schließen. Dieser Test wurde erst nur für die Startseite erstellt.

    it('should send me to the impressum page',() => {
     impressum.clickImpressumLink();
     common.switchToAnotherTab(1);
     expect(browser.getCurrentUrl()).toContain('/legal-notice');
     common.closeSelectedTab(1);
    })

Da das Impressum, laut Akzeptanzkriterium, von allen Unterseiten erreichbar sein muss, wurde dieser später in alle anderen Specs übertragen. Um den Code übersichtlich zu halten, wurde entschieden, diesen Test in ein Modul (impressum.ts) auszulagern.

    import { browser } from 'protractor';
    import { AppPage } from './app.po';
    import { CommonTabActions } from './common-tab-actions';
     
    module.exports = () => {
     let common: CommonTabActions = new CommonTabActions();
     new AppPage().clickImpressumLink().then(() => {
     common.switchToAnotherTab(1);
     expect(browser.getCurrentUrl()).toContain('/legal-notice');
     common.closeSelectedTab(1);
     });
    };

Die Verwendung in der e2e-spec.ts erfolgt auf diesem Wege:

    it('should send me to the impressum page', require('./impressum'));

Besonderheiten, Hinweise & Probleme

In jeder e2e-spec.ts können bestimmte vorgegebene Anweisungen geschrieben werden – z.B. beforeEach, beforeAll oder afterEach und afterAll. Wie die Namen schon sagen, wird der Code, der in einer dieser Anweisung steht, vor bzw. nach jedem oder allen Tests ausgeführt. In unserem Beispiel sollte jeder Test seinen eigenen Seitenaufruf haben. Somit kann z.B. die navigateTo Methode in die beforeEach Anweisung geschrieben werden. afterEach kann z.B. dafür genutzt werden, Tabs, die während der Tests geöffnet worden sind, wieder zu schließen.

Jeder Test beginnt mit dem Wort it. Wenn man vor diesem Wort ein x schreibt, also xit, wird dieser Test bei der Testausführung übersprungen. Es wird dann aber bei der Testausführung anders als bei einem auskommentierten Test mitgeteilt, dass ein oder mehrere Tests übersprungen worden sind. Sollte man einen Testfall mit f schreiben, also fit, werden nur noch Tests bei der Ausführung berücksichtigt, die auch ein fit davor stehen haben. Das ist nützlich, wenn man sehr viele Testfälle hat und man nur einige von ihnen laufen lassen will.

Beim Arbeiten mit Promise, die man aus manchen Methoden erhält, sollte man darauf achten, dass es bei falscher Handhabung zu asynchronen Fehlern kommen kann. Viele Ereignisse wie das Drücken eines Buttons oder die Abfrage, ob ein Element angezeigt wird, erzeugen als Wiedergabewert ein solches Promise. Selbst das Öffnen einer Seite gibt ein Promise<void> zurück. Um Fehler zu vermeiden sollte auf jedes Promise, welches weitere Aktionen nach sich zieht, wie das Drücken eines Buttons und die Ausgabe von einem dadurch entstandenen Wert, explizit mit then reagiert werden. Zum Beispiel:

    drückeButton().then( () => {
     gibMirDenEntstandenenWert();
    });
    //Wenn dieser Wert wieder ein Promise ist, der etwas auslösen soll, dann würde das Ganze so aussehen:
    drückeButton().then( () => {
     gibMirDenEntstandenenWert().then( () => {
     machNochEtwas();
     });
    });
    // oder etwas kürzer
    drückeButton()
     .then(gibMirDenEntstandenenWert)
     .then(machNochEtwas);

Für weitere Informationen zum Thema Promise siehe hier.

Fazit

Protractor eignet sich sehr gut für die Automatisierung von Oberflächentests in einem Softwareentwicklungsprojekt mit Angular2. Die Dokumentation seitens des Projektes ist sehr ausführlich und umfangreich. Durch die Nutzung von Selenium lassen sich die Tests ohne Probleme in den Buildprozess einbinden.

Die “Heisenbergsche” Testunschärfe bei automatisierten Testwerkzeugen

Kritische Fehler, die erst im Rahmen des Live-Betriebs öffentlich werden, stellen eine negative Werbung für ein Produkt und die beteiligten Unternehmen dar. Um dies zu verhindern, ist das Thema Testautomatisierung in der modernen Softwareentwicklung ein grundlegender und integraler Bestandteil. Durch die technische Umsetzung mit Testautomatisierungswerkzeugen entstehen aber Probleme, denen wir uns bewusst sein müssen.

Nur durch eine hohe Testabdeckung und der zeitnahen Rückmeldung von Ergebnissen lässt sich die Qualität und Reife des Produktes ausreichend genau nachweisen und bestätigen. Dabei werden von den Beteiligten verschiedene Testvorgehen und Testarten eingesetzt, wie automatisierte Codeanalyse oder automatisierte Unittests durch die Entwickler sowie automatisierte Oberflächentests durch die Tester. Für die unterschiedlichen Testarten wurde bereits früh versucht, übergreifende Kategorien zu finden, wie zum Beispiel die Abgrenzung in Black-Box- und White-Box-Tests.

Laut dem German Testing Board (Archivversion 2.1 des ISTQB® GTB Glossary) versteht man unter Black-Box-Testen “funktionales oder nicht-funktionales Testen ohne Nutzung von Informationen über Interna eines Systems oder einer Komponente” und unter White-Box-Testen einen „Test, der auf der Analyse der internen Struktur einer Komponente oder eines Systems basiert“. Bis vor ein paar Jahren ließen sich Black- und Whitebox-Testverfahren fast synonym zu zwei anderen Einteilungen benutzen: dem dynamischen Test, der „Prüfung des Testobjekts durch Ausführung auf einem Rechner“, und dem statischer Test, dem „Testen von Software-Entwicklungsartefakten, z.B. Anforderungen oder Quelltext, ohne diese auszuführen, z.B. durch Reviews oder statische Analyse“. Heute lässt sich diese Unterscheidung nicht mehr treffen, da Unit-Tests oder Testvorgehen wie Test-Driven-Development (TDD) die eigentliche Abgrenzung zwischen White- und Blackboxtests aufgelöst haben. Ich bezeichne den neuen Bereich als “Grey-Box-Test”. Die Grey-Box-Tests versuchen, erwünschte Vorteile von Black-Box-Tests (spezifikationsgetrieben) und White-Box-Tests (entwicklergetrieben) weitestgehend miteinander zu verbinden und gleichzeitig die unerwünschten Nachteile möglichst zu eliminieren.

Der Vorteil ist, dass Teilkomponenten und Gesamtsysteme mit dem geringen organisatorischen Aufwand der White-Box-Tests geprüft werden können, ohne eventuell “um Fehler herum” zu testen. So werden bei TDD die Komponententests anhand der Spezifikation vor der eigentlichen Entwicklung des Codes erstellt. Die Entwicklung der Komponenten wird erst abgeschlossen, wenn alle Prüfroutinen erfolgreich durchlaufen wurden. Neben den Vorteilen gibt es aber auch ein paar wichtige Aspekte zu beachten. TDD bzw. die Grey-Box-Tests erfordern eine hohe Disziplin, damit diese praktikabel und erfolgreich eingesetzt werden können. Aber viel wichtiger ist der Punkt, dass Grey-Box-Tests nicht unbedacht als vollwertiger Ersatz für Black-Box-Tests gesehen werden sollten.

Warum sollte man sich nicht nur auf automatisierte Grey-Box-Tests verlassen?

Grey-Box-Tests verändern und beeinflussen das System, das sie prüfen sollen. Dieser Aspekt ergibt sich aus der Natur des Tests. Denn was ist ein Test eigentlich? Er ist im Grunde ein empirischer Beweis. Wir stellen eine Hypothese auf und überprüfen diese in einem Experiment. Und in Analogie zu physikalischen Experimenten gilt auch für Softwaretests, dass je mehr ich mich dem Testobjekt nähere, das Ergebnis des Tests dadurch beeinflusst werden kann. Black-Box-Tests werden auf eigenen Testumgebungen durchgeführt, die einen ähnlichen Aufbau wie die Produktionsumgebung aufweisen sollten. Trotzdem bleibt es „ein Versuchsaufbau“. Es werden Mocks für fehlende Komponenten eingesetzt und der Log-Level erhöht, um mehr Informationen zu erhalten.

Grey-Box-Tests, also codenahe Tests, bei denen die zu testende Software ganz oder teilweise ausgeführt wird, sind nicht nur sehr nahe am Testobjekt. Mit Werkzeugen wie JUnit oder TestFX erweitern wir die Codebasis um neue Bestandteile. Es werden neue Zeilen Testcode geschrieben und Testframeworks als Library in die Softwarelösung eingebunden.

Aber auch bei Softwarelösungen wie QF-Test, Expecco oder Squish, die automatisierte Oberflächentests durchführen, rücken wir sehr nahe an das zu testende Objekt heran. Bei älteren Versionen der Automatisierungstools für graphische Oberflächen wurden die Aufnahme der Informationen dadurch erreicht, dass die Positionsdaten des GUI-Elements, wie zum Beispiel eines Buttons, gespeichert und zur Ausführungszeit ein entsprechendes Event abgesetzt wird. Anschließend erstellt die Software ein Screenshot und vergleicht jenes mit einem zuvor erstellten, um die Testergebnisse zu verifizieren. Also weitestgehend ungefährlich. Moderne Tools hingegen verfolgen einen anderen Weg. Sie verbinden sich über eine eigene „Engine“ mit der zu testende Applikation. Dadurch sind Sie in der Lage alle Control-Elemente der Oberfläche zu erfassen, deren Eigenschaften auszulesen und diese auch fernzusteuern. Die zugehörigen Daten werden als ein Modell der Applikation in so genannten GUI Maps abgelegt und sind die Grundlage für die anschließende Erstellung von Testskripten.

Die Auswirkung dieser Nähe zur zu testenden Software kann sein, dass bestimmte Fehlerwirkungen erst dadurch erzeugt oder, noch schlimmer, dadurch verschleiert werden bzw. nicht auftreten. Wir ändern die Grundlage des Tests durch den „komplizierten“ Testaufbau und können uns nicht sicher sein, dass die zu testende Software wirklich so reagiert hätte, wenn wir „nur“ manuell geprüft hätten.

Darum ist es wichtig, das Werkzeug und dessen Besonderheiten zu kennen sowie ein Bewusstsein für mögliche Fehlermaskierung durch die Codenähe der Tests zu haben. Wenn wir darin ein Risiko erkennen, sollten wir unsere automatisierten Tests durch andere manuelle Testarten ergänzen.