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.

Testautomatisierung mit Squish (Teil 2) – Fachliche Sicht

Bei einer aktiven Testautomatisierung wächst die Zahl der Testskripte oftmals täglich. Fehlen strukturelle Vorgaben, kann schnell die Übersicht und damit der Mehrwert der Testautomatisierung verloren gehen. Der Wartungsaufwand unstrukturierter Testskripte, die zum Teil nur noch von ihrem einstigen Autor verstanden werden, ist nicht zu unterschätzen und kann unter Umständen den gesamten Projektverlauf negativ beeinflussen.

Unter anderem deswegen wurde Python im ersten Teil als am besten geeignete Skriptsprache für die Testautomatisierung mit Squish bestimmt. Python besitzt wenige (für den Test) unnötige Zeichen und keine komplizierten Klammerungen, wodurch die Testskripte auch für Mitarbeiter ohne Fachkenntnisse gut lesbar sind. Die Klammerung wird in Python durch Zeileneinrückungen ersetzt, die immer aus vier Leerzeichen bestehen. Dies erhöht die Lesbarkeit des Testcodes zusätzlich.

Gerade in der Medizintechnik sind Testskript-Reviews ab und an erforderlich und seit neuestem sogar gefordert. Gut strukturierte Testskripte reduzieren den Zeitaufwand solcher Reviews enorm. Auch bei der Test-Fehleranalyse macht sich ein sauber aufgebautes Test-Framework positiv bemerkbar. Wie baut man also seine Testskripte auf?

Bei unserem Kunden hat sich die Vorgehensweise bewährt, zu allererst einen manuell durchführbaren Testfall mittels Testmanagement-Tool zu erstellen, so dass dieser per manuell geführtem Mauszeiger an der AUT („Application Under Test“) in der jeweiligen Testumgebung ohne Squish durchführbar wäre. Danach wird mit der Implementierung des Testskripts begonnen. Nicht nur aus Review-Sicht ist es hier empfehlenswert, die Testfall-ID gleichzeitig als Name des Testskripts zu verwenden (d.h. Testskript “tst_1134” würde Testfall mit ID 1134 automatisieren).

Die Testskripte an sich basieren auf einem Template, das im groben folgende Struktur aufweist:

    tstHelper = TstHelper()
     
    def main(): 
        try:
            preconditions()
            testprocedure()
            cleanUp()
        except Exception, e:
            tstHelper.fatal(e)
     
    def preconditions():
        'Vorbedingungen des Testfalls'
          
    def testprocedure():
        'Testschritte und erwartete Ergebnisse'
            
    def cleanUp():
        'AUT in den Ausgangszustand versetzen'

Wie in Teil 1 beschrieben wurde, genügt ein Einzeiler am Anfang, um die AUT zu starten bzw. sich mit dem laufenden Prozess zu verbinden, indem einfach ein Objekt der Klasse TstHelper instanziiert wird.  Weiterhin auffällig ist, dass sämtliche Vorbedingungen und Testschritte innerhalb eines try-except-Blocks abgehandelt werden. Alle unvorhergesehenen Exceptions, die während einer sauberen Testdurchführung nicht auftreten dürfen, werden abgefangen um eine individuelle Fehlerbehandlung zu starten.

Um einen fehlerhaften Zustand der AUT nicht zum Ausgangspunkt für nachfolgende Tests zu machen, sollte die Fehlerbehandlung darin bestehen, die AUT zu beenden und die Testumgebung aufzuräumen. So wird unter anderem vermieden, den Fehler mitzuschleifen und “False-Negative”-Testergebnisse bei nachfolgenden Tests zu erzeugen.

In der Methode “preconditions()” sind sämtliche, im manuellen Testfall beschriebenen Vorbedingen unterzubringen. Um die Wartbarkeit hoch zu halten, empfiehlt es sich, für jede Vorbedingung eine eigene Testskriptfunktion zu erstellen und diese so zu benennen, dass deren Name eindeutig deren funktionelle Inhalte widerspiegelt. Vorbedingung “User A ist ausgewählt” wird beispielsweise zur Testskriptfunktion “selectUser(“User A”)”, “Menü zur Konfiguration des Lichts ist geöffnet” wird zu “gotoConfigMenuLight()” usw.

Analog dazu wird mit Methode “testprocedure()” verfahren. Sie wird später alle Testschritte und erwarteten Ergebnisse enthalten. Zur besseren Strukturierung kann den einzelnen Testschritten zusätzlich ein kurzer Kommentar vorangestellt werden (z.B. “# 1.”). Auch hier sollte wieder, soweit möglich, pro Aktion im Testfall eine Testskriptfunktion geschrieben werden. Die Prüfung des jeweils erwarteten Ergebnisses sollte allerdings nicht innerhalb dieser Funktion, sondern separat aufgeführt gut lesbar im aufrufenden Testskript stehen. Dies erleichtert ein Review enorm und wirkt sich zudem positiv auf die Wartbarkeit der Skripte aus.

Eine 1:1-Beziehung zwischen Testskriptfunktion und Testschritt ist nicht immer möglich. Darum ist es auch zulässig, mehrere Funktionen im Testskript zu Blöcken zusammenzufassen und die Relation zum zugrundeliegenden Testschritt mittels passender Kommentierung herzustellen.

Ein großer Vorteil dieser Herangehensweise ist die automatische Trennung von technischer und fachlicher Testebene. Es entsteht ganz automatisch eine Bibliothek aus Testskriptfunktionen, die bei der Erstellung weiterer Testskripte wiederverwendet wird. Allerdings kann diese Bibliothek auch selbst wieder (technische) Fehler aufweisen. Häufig passiert es, dass Fehler bei der Testdurchführung nicht auf fachlicher sondern auf technischer Ebene zu suchen sind. Fehlerhaft implementierte Testskriptfunktionen können schnell zu “False-Negative”- bzw. “False-Positive”-Testergebnissen führen.

Um dem entgegenzuwirken, sollte eine eigene Testsuite erstellt werden, die ausschließlich die Bibliothek der Testskriptfunktionen im Sinne eines Unit-Tests prüft. Jede Testskriptfunktion sollte mindestens einmal auf alle möglichen Ein- und Ausgabewerte geprüft werden. Soweit möglich sollten die Tests wenige Fachlichkeiten enthalten und im Grunde genommen immer erfolgreich durchlaufen. Die AUT wird dabei nicht direkt getestet, sondern dient nur als Mittel zum Zweck, den Unit-Tests der Testskriptfunktion die passende Testumgebung zur Verfügung zu stellen.

Es empfiehlt sich zudem, diese Testsuite stets vor den eigentlichen produktiven Tests laufen zu lassen. Werden Fehler festgestellt, muss dies zum Abbruch der gesamten GUI-Testautomatisierung führen. So wird verhindert, dass technische Fehler in den Testskriptfunktionen “False-Negative”- bzw. “False-Positive”- Testergebnisse generieren, die wiederum einen erhöhten Aufwand bei der Fehleranalyse mit sich bringen würden (falls sie überhaupt gefunden werden).

Zusammenfassend kann gesagt werden, dass durch den hohen Funktionsumfang von Python nahezu jeder GUI-Test mit Squish automatisiert werden kann. Ein großer Pluspunkt ist zudem der hervorragende Support von Froglogic. Es ist selten vorgekommen, dass der Support länger als einen Tag zum Beantworten einer Anfrage gebraucht hat. Um den vollen Funktionsumfang von Squish ausschöpfen zu können, sind jedoch Programmiergrundkenntnisse zwingend erforderlich.

Der Maschine das Testen beibringen mit KI

Das Testen lässt sich heutzutage in zwei Kategorien aufteilen – das manuelle Testen und das automatisierte Testen. Dabei gewinnt der Bereich des automatisierten Testens immer mehr an Bedeutung. Wem kann dies auch verübelt werden? Durch den automatisierten Test lassen sich Testszenarien schneller durchführen und die Kosten für manuelle Tester senken.

Heutzutage wird das Thema Künstliche Intelligenz (KI) häufiger im Themenfeld der Qualitätssicherung betrachtet. Ist dies das Ende des manuellen Testens?

Immerhin wird aktuell an Software gearbeitet, die Programme selbstständig analysieren und passende Testfälle selbst schreiben kann. Zudem kann Testsoftware, die auf KI basiert, über Brute-Force ein viel weiteres Spektrum abdecken als es einem manuellen Tester möglich ist.

Doch bevor wir weiter auf den Vergleich zwischen manuellem Testen und Testen mit KI eingehen, sollten wir uns die Funktionsweise und Grenzen der KI anschauen.

Das logische Denken bei uns Menschen wird durch die neuronalen Verbindungen in unserem Gehirn gefördert. Mit demselben Konzept wird versucht, eine KI zu erstellen. Es wird ein neuronales Netz aufgebaut, welches sich über mehrere Ebenen entwickeln kann und mehrere Knoten besitzt.

Aufbau eines neuronalen Netzes
Abbildung 1: Aufbau eines neuronalen Netzes

Wie hier im Bild gezeigt wird, gibt es die Eingangs- und die Ausgangsknoten. Die Eingangsknoten kann man beispielsweise mit den Augen des Menschen vergleichen. Sie reagieren auf einen Reiz und verarbeiten diesen in einem Hidden-Layer mit unterschiedlichen Algorithmen. Die Ausgangsknoten geben die Reaktion wieder, auf die der Mensch reagiert. Genauso verarbeitet eine Software, die auf KI basiert, die Informationen.

Entsprechend der Aufgabe der KI muss die Anzahl der Eingangs- und Ausgangsknoten proportional steigen. Die Anzahl der Knoten und Ebenen im Hidden-Layer gibt die Komplexität des entstehenden Algorithmus an. Mit steigender Anzahl der Knoten steigt natürlich auch die hardwareseitig benötigte Rechenleistung. Nicht alle Knoten müssen miteinander verbunden werden. Die Knoten kann man zu bestimmten Aufgaben bündeln, indem nur eine gewisse Anzahl von Verbindungen zwischen den Knoten erstellt wird. So können z. B. die Algorithmen für das Sehen und das Hören erst einmal separat implementiert und im Nachhinein miteinander verbunden werden. Anschließend gibt es eine Reaktion auf die eingehenden Reize.

Bündelung der Knoten im neuronalen Netz
Abbildung 2: Bündelung der Knoten im neuronalen Netz

Nun haben wir also ein neuronales Netz aufgebaut. Doch wie funktioniert dieses Netz? Ganz simpel gesprochen könnte man sagen, dass die Eingangsknoten mit jedem Reiz eine „1“ signalisieren oder ohne Reiz eine „0“. Diese „1“ oder „0“ wird von jedem Knoten mit einem bestimmten Faktor multipliziert. Am Ende hat jeder Ausgangsknoten ein Ergebnis vorliegen und der Ausgangsknoten mit dem höchsten Wert startet die gewünschte Reaktion.

Aber woher kommen die Faktoren für die Knoten im Hidden-Layer? Hier kommt der Punkt ins Spiel, in welchem der Mensch nicht weggelassen werden kann. Eine durch KI gesteuerte Software kann zwar viele Vorgehensweisen und Möglichkeiten berechnen, doch weiß sie nicht, was richtig oder falsch ist. Würde ein Mensch einen Ball auf sich zufliegen sehen, würde er durch das, was er mit dem Auge sieht, sofort mit seinen Armen reagieren und den Ball fangen. Die KI könnte dies natürlich auch tun, aber sie könnte auch nichts tun, ausweichen, den Ball wegschlagen oder mit einer von vielen anderen Möglichkeiten auf den Reiz reagieren. Der Mensch selbst muss der KI erst einmal sagen, welche für die jeweilige Situation die richtige Reaktion ist. Dafür geben wir der KI mehrere Situationen vor und sagen ihr, wie sie dabei reagieren soll. Die KI passt dementsprechend die Faktoren ihrer Knoten an, um einen Algorithmus zu entwickeln. Der nächste Schritt wäre es, der KI Situationen vorzugeben, auf die sie selbst reagieren muss und der Mensch bewertet die Antwort der KI, um den Feinschliff des Algorithmus zu fördern. Erst ab diesem Punkt kann die KI allein arbeiten. Dieses Vorgehen nennt sich auch Deep-Learning, da die Tiefen des neuronalen Netzes geformt werden.

Hier kommt auch der Knackpunkt, warum das automatische Testen die manuellen Tester nicht komplett aus dem Verkehr ziehen kann. Jede KI muss auf die jeweilige Software erst einmal angepasst werden. Die KI selbst weiß nicht, worum es sich bei der Software handelt oder welche Reaktionen richtig sind.

Manuelle Einstellungen des Test-Programms
Abbildung 3: Manuelle Einstellungen des Test-Programms

Es müssen erst einmal die bedienbaren Felder definiert werden, die von der KI genutzt werden sollen. Es gibt natürlich auch Software, die sämtliche bedienbaren Objekte auf der GUI suchen und diese zum Testen nutzen kann. Doch auf diese Art und Weise würden per Brute-Force alle möglichen Kombinationen durchgeführt werden. Dementsprechend müssten von einem Tester eine Black- und eine White-List erstellt werden. Man kann natürlich der KI über mehrere Stunden freien Lauf lassen, in denen sämtliche Varianten der Einstellungshäkchen oder unzählige Textkombinationen im Namensfeld ausprobiert werden. Doch es wäre effizienter, die primären Testziele mit einer Eingrenzung zu fokussieren. Die KI liefert sämtliche Ergebnisse, die sie durch ihr stundenlanges Testen bekommen hat und gibt diese als Testszenarien aus, die neu geschrieben wurden und einen Failed- oder Passed-Zustand haben. Nun muss sich wieder ein Tester damit auseinandersetzen, bei welchen Tests es sich wirklich um einen Software- oder Bedienfehler handelt. Weiterhin können dann Testszenarien, die von der KI erstellt wurden, bei ihrem eigenständigen Testen gespeichert und später für automatisierte Tests weiterverwendet werden.

Es gibt heutzutage mehrere Firmen, die solche KI-Software anbieten wie z. B. Eggplant.io, Test.ai oder Retest. Außerdem entwickeln fast die Hälfte aller deutschen Softwarefirmen ihre QA-Abteilung im Bereich der KI weiter.

Der World Quality Report fasst Ergebnisse einer weltweiten Befragung zur Anwendungsqualität und Testing-Verfahren zusammen. In der aktuellen Ausgabe wird darüber berichtet, dass KI im Bereich intelligenter Automatisierung als wichtigster Punkt für die Verbesserung der Qualitätssicherung in den nächsten zwei bis drei Jahren herangezogen wird.

Ich hoffe, ich konnte mit diesem Blogbeitrag einen kleinen Einblick in das Testen mit KI geben und zeigen, dass die Automatisierung einen großen Schritt mithilfe von KI macht. Dennoch sind manuelle Tester dadurch noch nicht zum Aussterben verdammt – was uns die Zukunft bringen wird, kann jedoch aktuell noch niemand genau sagen.

Testautomatisierung mit Squish (Teil 1) – Technische Sicht

Am Markt existiert mittlerweile eine Vielzahl von Testautomatisierungswerkzeugen für die verschiedensten Einsatzgebiete. Bei einem unserer Kunden aus der Medizintechnik wurde beispielsweise das Tool „Squish“ intensiv zur Automatisierung von GUI-Tests eingesetzt. In diesem Beitrag möchte ich deshalb näher auf die dabei zu beachtenden technischen und fachlichen Aspekte beim Design des Testframeworks und der Testskripte eingehen. Auch im zweiten Teil der Blogpost-Reihe gibt es mehr darüber zu erfahren.

Beim GUI-Testautomatisierungstool “Squish” der Hamburger Softwareschmiede Froglogic wird der gesamte Testcode mit allem, was dazu gehört, in einer von mittlerweile fünf gängigen Programmier- bzw. Skriptsprachen verfasst und verwaltet. Neben Ruby und JavaScript stehen hier Tcl und Perl zur Auswahl. Aufgrund seiner Aktualität und seines mächtigen Funktionsumfangs, der durch die zahlreichen, frei erhältlichen Libs noch erhöht werden kann, aber vor allem wegen der ausgesprochen guten Lesbarkeit der damit erzeugten Testskripte sollte Python das Mittel zur Wahl werden. Standardmäßig wird Squish mit einem Python 2.7.XX ausgeliefert, auf individuelle Nachfrage stellt Froglogic allerdings auch gern eine Squish-Edition mit der gewünschten Python-Version (z.B. Python 3.5.XX) zum Download bereit. Wer auf Python 3 schwört, kann diese Möglichkeit gern in Betracht ziehen, mit dem mitgelieferten Python 2.7 ist man jedoch bestens bedient.

Ausschnitt Squish IDE basiert auf der Open-Source IDE Eclipse
Abbildung 1: Squish IDE basiert auf der Open-Source IDE Eclipse

Unabhängig von der gewählten Skriptsprache bietet Squish generell zwei Ansätze für den Umgang mit einer AUT (“Application Under Test”) zur Testdurchführung. Entweder die AUT wird für jeden Testfall durch Squish implizit gestartet und gestoppt oder und man verbindet sich für jeden Testfall neu zu einer bereits laufenden AUT. Da die meisten Softwareanwendungen in der Praxis nicht fortlaufend beendet und neu gestartet werden, kommt der letzte Ansatz einem Verhalten in der Realität näher und ist ersterem unbedingt vorzuziehen.

In der Squish-Welt wird dieser Ansatz auch als “Attachable AUT” bezeichnet. Die Testskriptfunktionen zum Steuern einer “Attachable AUT” werden von Froglogic jedoch nur zum Teil zur Verfügung gestellt und müssen selbst implementiert werden.

Über Jahre bewährt hat sich bei unserem Kunden hier der “TstHelper”. Wie der Name andeutet, handelt es sich dabei um eine Helferklasse für die Testdurchführung, die u.a. einen Mechanismus zum Handling des “Attachable AUT”-Ansatzes implementiert. Um überflüssigen Testcode in einem Testskript zu minimieren, wurde der gesamte Mechanismus im Konstruktor untergebracht. Ein Einzeiler, der ein Objekt der Klasse “TstHelper” zu Beginn eines Testskripts instanziiert, ist somit ausreichend – dazu mehr im zweiten Teil.

Im Prinzip besteht der Mechanismus nur aus einem einzigen “try-except”-Block:

    try:
         attachToApplication()
    except RuntimeError:
         AppUnderTest.start() 

Ein “RuntimeError” wird von der Squish-Funktion “attachToApplication” genau dann geworfen, wenn sich zu einer AUT verbunden werden soll, die noch nicht gestartet wurde. Dann wird die statische Funktion AppUnderTest.start() aufgerufen, die – wie der Name schon vermuten lässt – die AUT startet. Sowohl die Klasse als auch die Funktion müssen selbst implementiert werden. Der Name “AppUndertest” sollte durch den Namen der tatsächlich zu testenden Applikation ersetzt werden. Dieser Name ist dann zugleich der Name des Namespace, der die Funktion start() bereitstellt.

In Python existiert keine eigene Notation für Namespaces, weshalb Namespaces mittels Klassen realisiert werden. Vereinfacht gesehen sollte diese Klassenstruktur folgendermaßen aussehen:

    class AppUnderTest:
        
        @staticmethod
        def start():
             os.system("{BatchSkript}")
         
        @staticmethod
        def stop():
             ToplevelWindow.byName("{MainWindowObjID}", 10).close()

Beim “Attachable AUT”-Ansatz läuft die AUT in einem eigenen Prozess unabhängig von Squish. Das Starten erfolgt daher über ein externes Batch-Skript, das einmalig zu erstellen ist. Die Einbindung des Skript-Aufrufs erfolgt dann in der start()-Funktion mittels des Python-Kommandos “os.system” (s.o.).

Zum Stoppen der AUT bringt Squish die Funktion “ToplevelWindow.byName(“{MainWindowObjID}”, 10).close()” mit. Der Parameter “MainWindowObjID” repräsentiert dabei die Objekt-ID des hierarchisch gesehen obersten Elements aus der Object-Map. Der Funktionsaufruf wird in der statischen Funktion stop() gekapselt. Ihrem Aufruf im Testskript muss demzufolge ebenfalls der Klassenname vorangestellt werden: AppUnderTest.stop(). Diese Syntax wurde bewusst gewählt wegen ihrer guten und eindeutigen Lesbarkeit. Alle Funktionen, die in Verbindung mit der AUT stehen, sollten in dieser Klasse bzw. diesem Namespace zusammengefasst werden. Es können Funktionen ergänzt werden, um etwa die AUT in ihren Ausgangszustand zurückzuversetzen, auf bestimmte System-Ereignisse zu warten bzw. zu reagieren oder den “attachToApplication()”-Aufruf zu kapseln, um eventuell Logging hinzuzufügen.

Die Organisation in Namespaces eignet sich zudem ideal zur Einbindung zusätzlicher Testtools, die aus einem Testskript heraus gesteuert werden sollen. Für jedes Testtool wird eine eigene Python-Klasse nach obigem Schema erstellt. In den start()- und stop()-Methoden ist jeweils der Aufruf zum Starten und Stoppen des Testtools unterzubringen. Auch diese Methodenliste kann beliebig erweitert werden, z.B. um Funktionen zum Sichern der Logfiles etc. Im Testskript erfolgt deren Aufruf dann analog mittels “Testtool.start()” bzw. “Testtool.saveLogfilesTo()”. Ich denke, es muss nicht erwähnt werden, dass der Klassenname durch den Namen des Testtools ersetzt werden sollte. Dadurch ergibt sich eine Syntax wie beispielsweise “CanSimuator.start()”, was zur erhöhten Lesbarkeit und somit zu einem erleichterten Testskript-Review beiträgt – mehr dazu im zweiten Teil der Blogpost-Reihe.

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.

Desktop vs. Web – DWX 2016

Auch dieses Jahr war die Saxonia Systems AG (seit 03/2020 ZEISS Digital Innovation) wieder auf der Developer Week in Nürnberg vertreten. Mit insgesamt fünf Vorträgen und einer Abendveranstaltung widmeten wir uns Themen wie „agiles Testen“, „Testautomatisierung“ und „agilen Architekturen“. Zeitgleich haben wir die Chance genutzt, um ein aktuelles Meinungsbild der Besucher zu erhalten.

In diesem Zusammenhang haben von den mehr als 1600 Besuchern 131 Personen auf unsere Fragen geantwortet. Die Umfrage ist demnach nicht repräsentativ für die gesamte Branche, gibt aber eine gewisse Aussicht und Einschätzung innerhalb der Microsoft Community.

Unsere erste Frage lautete: „Entwickeln Sie Endkunden- oder Businesssoftware?“. Die Mehrheit der Befragten gab an, dass sie an Software für Geschäftskunden arbeitet.

Welche Art von Software entwickeln Sie? - Diagramm
Abbildung 1: Welche Art von Software entwickeln Sie?

Die nächste Frage lautete: „Wie lange besteht die Software, an der Sie arbeiten?“. Die Umfrage brachte folgende Ergebnisse:

Alter von Softwaresystemen - Diagramm
Abbildung 2: Alter von Softwaresystemen

Anhand des Ergebnisses sieht man, dass bestehende Software meist bis zu 20 Jahre alt ist. 68% der Systeme besitzen ein maximales Alter von bis zu 10 Jahren. Dies kann zum einen damit zusammen hängen, dass Version 2.0 von .Net, welche den breiten Durchbruch einläutete, erst 2006 veröffentlicht wurde und somit das Gros der heute noch genutzten .Net Anwendungen kaum älter sein kann. Auch wenn nicht danach gefragt wurde, so zeigt sich in Gesprächen, dass viele der noch älteren Anwendungen ehemalige C++ Desktopanwendungen auf Basis von MFC sind, die nach und nach zu .Net migriert wurden.

Damit sind wir auch bei der nächsten Frage: Welche Art von Technologien verwenden Sie eher?

Welche Technologien verwenden Sie eher? - Diagramm
Abbildung 3: Welche Technologien verwenden Sie eher?

Hierbei ging es insbesondere darum, zwei Vermutungen auf den Grund zu gehen. Erstens sind Webanwendungen im Durchschnitt jünger als Desktopanwendungen und zweitens finden Desktopanwendungen mehr Einsatz im Geschäftskundenumfeld.

Auch wenn nur 33 der 48 befragten Webentwickler diese  Frage beantworteten, so zeigt sich zumindest ein klarer Trend zu jüngerer Software im Web. Dies lässt sich natürlich leicht darauf zurückführen, dass die Nutzung des Webs vor 20 Jahren nicht ansatzweise dem heutigen Stand entsprach und somit auch nicht annähernd die Bedeutung innerhalb von Geschäftsprozessen hatte.

Alter von Webapplikationen - Diagramm
Abbildung 4: Alter von Webapplikationen

Um Vermutung zwei zu untersuchen, müssen wir nun die bisherigen Zahlen gegenüberstellen. Hierbei kann man, allein schon wegen der geringen Stichprobe, nicht von einem eindeutigen Trend sprechen. Wenig überraschend ist natürlich, dass man im Geschäftskundenumfeld eine Art Mischbetrieb eher verfolgt, als bei reiner Endkundensoftware. Ansonsten sind die Zahlen so nah beieinander, dass der Trend zumindest für Microsoft basierte Anwendungen nicht nachzuweisen ist.

Gegenüberstellung des Technologie-Stacks bei Business-Software und Endkunden-Software - Diagramm
Abbildung 5: Gegenüberstellung des Technologie-Stacks
bei Business-Software und Endkunden-Software

Der weite Einsatz von Desktoptechnologien ist auch insofern verständlich, da Microsoft basierte Technologien ihre Ursprünge auf dem Desktop haben. Lange Zeit galt es daher als gesetzt, dass man Desktopanwendungen am besten mit Microsoft Tools entwickelt. Das Web wurde erst einige Jahre später in Angriff genommen und hier ist Microsofts Konkurrenz mit Java, Ruby oder PHP auch ungleich größer.

Wenn das Web nun aber so bedeutsam ist und die Desktopanwendungen vergleichbar alt sind, dann liegt der Schluss nahe, dass zukünftig auch vom Desktop in das Web migriert werden soll. Auf diesen Gedankengang zielte dann auch unsere nächste Frage ab.

Ist es geplant Desktoptechnologien auf Webtechnologien zu migrieren? - Diagramm
Abbildung 6: Ist es geplant Desktoptechnologien auf Webtechnologien zu migrieren?

Eine so klare Aussage hat uns stark überrascht. Natürlich ist der Trend ins Web in aller Munde, immerhin schwächelt der Absatz des Desktops seit Jahren. Wenn nun aber bei 46 von 96 Entwicklern zukünftig die Desktopclients durch Webclients ersetzt werden sollen, ist dies ein mehr als deutliches Zeichen. Hier wird auch offensichtlich, wie wichtig Microsofts Trend zur Cloudplattform Azure ist, da die Akzeptanz ihres früheren Stammgeschäfts dramatisch sinkt.

Vergleichen wir nun noch die eingesetzten Sprachen, welche uns eventuell einen zukünftigen Trend aufzeigen könnten.

Eingesetzte Sprachen im aktuellen Hauptprojekt - Diagramm
Abbildung 7: Eingesetzte Sprachen im aktuellen Hauptprojekt

Deutlich an erster Stelle und mit Nennung durch fast alle Teilnehmer ist die Sprache C#. Dies wundert nicht, wenn man die Bedeutung der Sprache für das Ökosystem kennt. Viel interessanter ist das schlechte Abschneiden von Visual Basic im Vergleich. Dies wird von noch weniger Personen eingesetzt als C++ und Java. Wobei deren geringe Nennung natürlich damit zu tun hat, dass es sich bei der Developer Week um eine Konferenz handelt, die einen sehr großen Anteil von Microsoftthemen besitzt. Auch der Einsatz von Javascript ist wenig verwunderlich, wohingegen die deutliche Nennung von Type Script als ein Ausblick auf dessen zukünftige Bedeutung darstellen könnte.

Hier könnten wir etwas in die Glaskugel schauen, denn aus den bisherigen Auswertungen lässt sich durchaus folgendes Szenario konstruieren: Während reine Desktopclients an Bedeutung verlieren, ziehen Webtechnologien mit Angular und der Laufzeitumgebung Electron durchaus auch auf dem Desktop ein. Angular wiederum bietet vieles von dem, was WPF einst einzigartig gemacht hat und weniger von dem, wodurch sich AngularJS 1 für Desktopentwickler doch vergleichsweise unangenehm angefühlt hat. Gerade auch mit der nahtlosen Integration von Type Script gewinnt Angular viele der Sicherheitsmechanismen, die skalierbare Großanwendungen benötigen. Es könnte also durchaus passieren, dass so mancher Desktop Client seinen Weg über das Web zurück auf den Desktop schafft, nur in neuem Gewand.

Gegenüberstellungen der Sprachen bei Business-Software und Endkunden-Software - Diagramm
Abbildung 8: Gegenüberstellungen der Sprachen bei Business-Software und Endkunden-Software

Abschließend betrachten wir noch den Einsatz der verwendeten Frameworks. Auch hier muss man Vorsicht walten lassen, da die geringe Teilnehmerzahl natürlich eine deutliche Unschärfe bedeuten kann.

Eingesetzte Frameworks im Hauptprojekt - Diagramm
Abbildung 9: Eingesetzte Frameworks im Hauptprojekt

Nichtsdestotrotz ist dabei interessant, wie häufig Angular 2 bereits in Hauptprojekten eingesetzt wird und dass diese Zahl fast den 17 Personen entspricht, die Typescript als Sprache angegeben haben. Dem gegenüber haben im Web die ASP.Net WebForms im Verhältnis zu ASP.Net MVC weit deutlicher verloren, als es die Windows Forms gegenüber der WPF getan haben.

Auch hier zeigten sich in den Gesprächen auf der Konferenz einige deutliche Einschätzungen: WebFroms haben gegenüber MVC keine Vorteile, wohingegen WinForms gegenüber WPF eine geringere Lernkurve und eine höhere Performanz aufweisen. Dem gegenüber bietet die WPF wiederum ein schwerer zu meisterndes Architekturmuster, welches auf Dauer aber besser wartbare Software erlaubt. Darüber hinaus kann sie weitaus einfacher gestylt werden und ermöglicht eine bessere Parallelisierung innerhalb des Teams.

Genau hier könnte sich ein Hindernis für das Glaskugel-Szenario herauskristallisieren, denn gerade auch durch die Einbindung von Win32 Applikationen in die Universal App Plattform und durch den hohen Reifegrad der Desktopapplikationen, weisen sie Vorteile gegenüber den schnelllebigen Webtechnologien auf, welche in langfristig orientierten Umgebungen nicht einfach außer Acht gelassen werden können. Hinzu kommt noch, dass Angular 2 ebenfalls eine sehr steile Lernkurve aufweist, die erst einmal gemeistert werden möchte.

Kochrezepte für Testautomatisierung (Teil 2) – Datensalat

Testomaten auf Datensalat an Stressing

Eine besondere Herausforderung für jede manuelle Testdurchführung und ganz besonders für die Testautomatisierung sind die Testdaten. Bei den meisten manuellen Tests stehen in den Testfällen meist nur grobe Hinweise zu den zu verwendenden Testdaten. Das Vorgehen funktioniert in der Testautomatisierung nicht.

Im vorhergehenden Thema „Zutaten und Küchengeräte für Testomaten und wer ist der Koch“ habe ich darüber berichtet, welche Voraussetzungen für die Testautomatisierung gegeben sein müssen, um erfolgreich Automatisierung einzusetzen. Dabei habe ich bereits auf eine weitere Herausforderung hingewiesen: Die Testdaten. Dieses Thema möchte ich in diesem Blogeintrag nun etwas näher beleuchten.

Was passiert, wenn wir uns nicht um die Testdaten bei der Testautomatisierung kümmern und uns auf Testdaten stützen bei denen die Testautomatisierung nicht berücksichtigt wurde?

Testen kann man mit Kochen vergleichen. Das Rezept ist der Testfall und die Testdaten sind die Zutaten. Beim manuellen Testen folgen wir dem Rezept/Testfall und suchen die Zutaten/Testdaten nach Bedarf. Dies funktioniert beim automatisierten Testen nicht. Hier müssen die Testdaten bzw. die Zutaten exakt und vorportioniert vorliegen. Das heißt, es reicht nicht nur die Art und Form der Testdaten zu benennen, sondern es ist notwendig das genau die Instanz des Testdatums im Testskript benannt ist.

Zusätzlich werden beim Testen die Testdaten verbraucht bzw. die Testdaten altern. Also wie im Restaurant, irgendwann sind die Tomaten alle oder der Blattsalat wird welk. Wie bekommen wir also unsere passenden Zutaten und dies auch in einer ausreichenden Menge.

Oft höre ich in Projekten: „Wir machen einfach einen, hoffentlich anonymisierten, Produktionsabzug“. Der liefert jedoch nur einen Teil der benötigten Daten für die Testautomatisierung. Das heißt, für unveränderliche Stammdaten  ist so ein Abzug sehr wohl nützlich. Nun bekommt der Koch in der Küche nicht immer die gleiche Bestellung für seinen Salat. Mal möchte der Gast den Salat mit oder ohne Oliven, mal mit Joghurtdressing, mal mit Essig und Öl. In Abhängigkeit der Zutaten bzw. Änderungen an der Bestellung ändert sich dann auch noch der Preis. Das heißt wir benötigen für unsere Testdaten auch veränderliche Daten so genannte Bewegungsdaten, um die Geschäftsprozesse vollständig abzubilden. Um die benötigten veränderlichen Daten für die Testautomatisierung bereit zu stellen gibt es zwei Ansätze und beide haben ihre Vor- und Nachteile. Beim ersten Ansatz werden die benötigten Daten aus dem anonymisierten Produktionsabzug herausgesucht. Der Aufwand die entsprechenden Filterkriterien zu ermitteln und anschließend in einer Abfrage zu formulieren kann jedoch schnell sehr hoch ausfallen. Der Nachteil ist dabei, dass die herausgefilterten Daten nur in begrenzter Anzahl zur Verfügung stehen und werden bei der Testdurchführung ggf. verbraucht.

Beim zweiten Ansatz werden die benötigten Testdaten in der Datenbank neu erstellt, so dass der Testfall über alle notwendigen Testdaten verfügt. Diese Testdaten werden zwar auch verbraucht, können jedoch durch die automatisierten Skripte immer wieder neu angelegt werden. Auch das Anlegen der so genannten synthetischen Testdaten kann, z. B. bei vielen abhängigen Daten, sehr aufwendig sein. Daher ist immer individuell abzuwägen, welche der beiden Ansätze für welchen Testfall zum Einsatz kommt.

Bei der Testautomatisierung ist es auch oft notwendig die Testdaten zu dynamisieren. Was heißt das? Nehmen wir wieder ein Beispiel aus der Küche. Bekanntlich muss eine gute Pekingente 24h vor dem Verzehr bestellt werden. Da das Verzehrdatum ein veränderliches Datum ist kann hier eine Dynamisierung die Lösung für die Automatisierung bringen. Das Ergebnis sieht dann so aus: Verzehrdatum = Bestelldatum + 24h. Solche oder ähnliche dynamischen Testdaten kommen auch bei der Testautomatisierung zum Einsatz.

Fazit, in den meisten Fällen ist es wie bei einem guten Salat – die Mischung macht‘s. Also hier nochmal das Rezept zusammengefasst. Man nehme als erstes einen anonymisierten Produktionsabzug für die grundlegenden und unveränderlichen Stammdaten (diese können auch bei kleineren Mengen selbst erstellt werden), dazu einige gut ausgewählte veränderliche Daten aus dem Produktionsabzug mittels Abfrage im Testfall. Nun noch eine gut ausgewählte Portion synthetische Testdaten. Das ganze gut abgestimmt mit dem Testfalltemplate. Zum Schluss noch ein Schuss dynamisierte Testdaten im Template oben drauf und fertig ist der perfekte Datensalat für unsere Testautomatisierung. Wichtig nach dem Anrichten der Testdaten in der Datenbank muss die Datenküche auch wieder gut aufgeräumt werden. Also alle statischen, synthetischen und dynamischen Testdaten in den Ausgangszustand bringen, falls sie durch den Testfall verändert wurden. Im Klartext bedeutet dies, dass jeder Testfall in der Nachbedingung ein Aufräumen der Testdaten beinhaltet, denn Sauberkeit ist in einer guten Datenküche oberstes Gebot.

Wie ja bekannt ist, kommen in der Küche auch verschiedene Geräte zur Automatisierung des Kochvorgangs zum Einsatz. Wie viele dieser Testautomaten für ein Projekt gut sind, behandeln wir in meinem nächsten Blogbeitrag aus der Reihe Kochrezepte für Testautomatisierung. Bis dahin happy Testing und immer eine gut aufgeräumte Datenküche.

Kochrezepte für Testautomatisierung (Teil 1) – Suppe

Zutaten, Küchengeräte und wer ist der Koch?

Ein Kollege sprach mich kürzlich an und fragte, ob ich ein Rezept für eine gute Testautomatisierung kenne. Ich sagte, dass man dafür – wie für eine gute Suppe – nicht nur ein Rezept braucht, sondern es kommt auf die Ausstattung der Küche, die Zutaten und den Koch an. Entscheidend für die Testautomatisierung sind also die Projektrahmenbedingungen, die Auswahl der Testwerkzeuge und die Tester die an der Testautomatisierung beteiligt sind.

Wie können wir feststellen, ob die Rahmenbedingungen (nicht verwechseln mit Ramenbedingungen) für die Testautomatisierung gegeben sind? Grundlegend ist hierbei zu unterscheiden, ob sich das Software-Projekt noch in der Vorplanung befindet und die Testautomatisierung von Anfang an eingeplant wird oder ob es sich um ein Projekt handelt, welches bereits läuft und mit Testautomatisierung unterstützt werden soll.

Eines schon vorweg gesagt – je früher mit einer gut geplanten Testautomatisierung begonnen wird, desto wahrscheinlicher wird es eine erfolgreiche Testautomatisierung. Die Gründe liegen oft darin, dass die anfänglichen Aufwände der Testautomatisierung unterschätzt werden und sich der Nutzen rein rechnerisch erst nach Projektende einstellt.

Um dem zu begegnen ist für ein beginnendes Projekt eine gute Planung und für ein laufendes Projekt erst einmal eine Vorstudie der laufenden Prozesse, Testfälle und Rahmenbedingungen notwendig. In so einer Vorstudie sind unter anderem das bereits vorhandene Testfallportfolio, das Testobjekt und die Kenntnisse der Tester zu analysieren und zu bewerten. Auf diese Weise können wir erkennen, wo die eigentlichen Schwachpunkte des Projektes im Testbereich liegen.

Oft wird versucht mit Testautomatisierung Probleme zu lösen, die nichts mit der Testautomatisierung zu tun haben bzw. gegebenenfalls dadurch noch verschlimmert werden. Das ist wie beim Kochen, wenn die Suppe nicht schmeckt wird diese nicht zwingend besser, wenn wir einfach den Koch durch einen Automaten austauschen. Oft wird das Ergebnis sogar schlechter, wenn ich den Kaffeeautomaten im Flur betrachte. Wichtig ist, dass die Voraussetzungen für die Testautomatisierung eine Mindestqualität erreicht haben, bevor mit der Toolauswahl oder gar der Automatisierung begonnen werden kann. Die Testautomatisierung ist der Sahneklecks auf der Suppe, d. h. Testautomatisierung ist nur in einem gut funktionierenden Testprozess erfolgreich. Auch die Testfälle müssen eine ausreichende Detailtiefe zur Automatisierung haben. Die benötigte Detailtiefe ist davon abhängig in wie weit der Tester die nötigen fachlichen Kenntnisse zum Testobjekt mitbringt. Hier könnte der eine oder andere auf die Idee kommen gleich die Fachseite – also die die das Fachwissen haben – die Testautomatisierung machen zu lassen. Das ist meist keine gute Idee, da den Mitarbeitern die Zeit fehlt. Zum einen steht ihre tägliche Arbeit in der Priorität immer höher als die Testautomatisierung und zum anderen müssen die Toolkenntnisse und Testerfahrungen erst aufgebaut werden. Daher empfiehlt es sich, dass die Fachseite die manuellen Testfälle in einer ausreichenden Detailtiefe liefert, welche der Tester auch ohne Domainwissen versteht. Anschließend nimmt der Tester mit seiner Testerfahrung und der nötigen Toolkenntnis die Automatisierung vor.

Es ist illusorisch alles automatisieren zu können, darum wird in der Vorstudie ein Priorisierung der Testfälle vorgenommen. Dabei ist auch das Testobjekt bzw. die zu testende Software auf Automatisierbarkeit zu prüfen. Besonders problematisch für die Testautomatisierung sind z. B. dynamische Objekt-IDs, die sich bei jedem erneuten Aufruf ändern.

Wie wir auch aus der Küche bereits kennen, ist dann auch noch der Koch wichtig, d. h. der Tester, der die Testautomatisierung durchführt. Zum einen benötigt dieser das nötige Toolwissen und zum anderen muss er auch entscheiden können, wo sich die Testautomatisierung im Einzelnen lohnt. Zum Beispiel macht es keinen Sinn eine Funktion in der Software zu automatisieren, die nur extrem selten benötigt wird und im Automatisierungsaufwand sehr hoch ist.

Darum sollte man sich immer an das Rezept halten:

  • mit gutem Augenmaß und einem Gespür für den Aufwand eine ausgewogene Komposition – Kosten und Nutzen im Auge behalten
  • nicht nur auf die Technik verlassen – auch das eigene Handwerk ist wichtig
  • Testautomatisierung funktioniert nur, wenn vorher die grundlegenden Testaufgaben und Prozesse funktionieren – erst Zwiebeln schneiden lernen und dann mit der Bouillabaisse anfangen

Eine weitere Herausforderung für die Testautomatisierung sind die Testdaten. Hierzu aber mehr in meinem nächsten Blogbeitrag. Bis dahin wünsch ich schon mal ein Frohes Fest und überlasst die Weihnachtsgans nicht achtlos einem Automaten.