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.

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.