Entwickler entdecken UI-Testing

von Mark Michaelis

Eine hohe Kunst der Testautomatisierung sind Tests für grafische Benutzeroberflächen. Oftmals verbergen sich hinter ihnen komplexe Systeme, die es innerhalb der Tests zu kontrollieren gilt, und gleichzeitig sind die Oberflächen selbst bereits schwer zu kontrollieren wie etwa bei AJAX-Oberflächen.

Dieser Artikel beschreibt, wie wir diesen Herausforderungen begegnet sind und wie wir heute erfolgreich GUI-Tests schreiben. Welche Prozesse waren nötig, welche Anforderungen und Herausforderungen gab es zu meistern, welche Empfehlungen ergeben sich für andere GUI-Tests?

Auch wenn die Technik sich auf Java und Selenium WebDriver bezieht – die Erkenntnisse sind auf alle anderen GUI-Tests übertragbar.

UI-Testing

Eine grafische Benutzeroberfläche (auch GUI – graphical user interface, im Folgenden kurz als UI bezeichnet) ist heute Standard beim Zugriff auf teilweise komplexe Serversysteme. Wo früher Kommandozeilen als Zugriff auf solche Systeme standen, ging der Weg über relativ schwergewichtige UIs, die auf dem Desktop liefen, bis zu den heutigen UIs, die im Browser oder als App auf mobilen Endgeräten laufen. Unser System ist ein solches Serversystem – ein Web-Content-Management-System mit einer im Browser laufenden UI, um die Inhalte zu redaktionieren.

Ebenso komplex wie unser System sind auch unsere Tests. Von Unit-Tests über Integrationstests bis hin zu Tests der UI. Fokus dieses Artikels sind die Tests für unsere RedaktionsUI, welche im Browser als Rich Web Application läuft: das CoreMedia Studio, im Folgenden kurz Studio genannt.

Die Automatisierung dieser UI-Tests stellt eine besondere Herausforderung dar – und nichtsdestotrotz ist die Automatisierung für uns unverzichtbar, da wir gleichzeitig unsere Entwicklungs- und Release-Zeit beschleunigen wollen. Wo früher mehrwöchige QA-Phasen standen, muss nun am Ende eines zweiwöchigen Entwicklungssprints ein hochwertiges Inkrement unserer Software zur Verfügung stehen. Intern bauen wir inzwischen täglich ein Release, und jeden Tag benötigen die Teams den Status der automatischen Tests.

Der Beginn einer Reise

Unsere Entwicklungsmannschaft besteht aus etwa 40 Entwicklern, die in mehrere Teams aufgeteilt sind. Alle Teams, verteilt über mehrere Räume, sind auch an der Entwicklung der Redaktions-UI beteiligt und sind aufgefordert, die Software über automatische Tests abzusichern – auch UI-Tests.

Ein Teammitglied schlüpft mal in die Rolle eines Entwicklers und mal in die Rolle eines Testers. Eine QA-Abteilung existiert nicht. Dieses Konzept, die QA direkt in die Entwicklung zu integrieren, hat sich seit Jahren bei uns bewährt. Pair-Programming und ReviewProzesse nutzen wir, um einer möglichen Betriebsblindheit beim Testen zu begegnen. Im Folgenden werde ich diese Teammitglieder als Entwickler bezeichnen. Denn auch wenn sie in der Rolle eines Testers unterwegs sind, so sind doch alle mit Leib und Seele Entwickler.

Jeder Entwickler ist wohlvertraut mit Testwerkzeugen wie Unit-Testing auf Basis von JUnit [1] und dem Einsatz von JUnit im Rahmen von Systemtests. Mit dem Einzug der UITests in die Entwicklungsteams betraten die Entwickler Neuland, und jedes Team ging für sich auf eine Entdeckungsreise.

Allen gemeinsam war die Basis für die UITests: Selenium [2] – das wohl bekannteste Testframework für Browser-UI-Tests. Es gab aber auch sehr starke Unterschiede in der Implementierung der Tests:

  • Selenium 1 Tests mit einer eigens entwickelten Abstraktionsschicht, welche die Zugriffe auf die UI kapselte,
  • Selenium 2 (WebDriver) BDD Tests auf Basis von JBehave [3] und unter Verwendung einer Abstraktionsmethode, die WebDriver zur Verfügung stellt: PageObjects, sowie
  • diverser Mischformen.

Um das Wissen über Selenium unter den Teams auszutauschen, entstand eine Community of Practice [4], ein Treffen Gleichgesinnter aus verschiedenen Teams zum Wissensaustausch: die Selenium-Gruppe. Der Austausch war jedoch aufgrund der Unterschiede schwierig. Es gab Ansätze, Testwerkzeuge anderer Teams wiederzuverwenden und zu erweitern. Die Schwierigkeiten stellten sich durch die unterschiedlichen Basistechnologien ein und dadurch, dass die Testwerkzeuge nicht mit dem Fokus einer Wiederverwendung außerhalb eines Teams konzipiert worden waren.

Jedes Team verfügte zudem über unterschiedliche Ansätze, Problemen beim UI-Testen zu begegnen:

  • Wie findet man Elemente in der UI?
  • Wie wartet man auf Aktualisierungen in der UI?
  • Wie bereitet man ein System für UI-Tests vor?
  • Auf welcher Infrastruktur lässt man die UI-Tests laufen?
  • Wie hält man eine Infrastruktur stabil?

Ich reiste durch die Teams und nahm die Unterschiede auf, lernte Vor- und Nachteile kennen und erarbeitete zusammen mit den Entwicklern eine Vision, wie wir am Ende der Entdeckungsreise das Gelernte zu einem neuen, gut verstandenen und teamübergreifenden Testframework zusammenführen können.

Warum UI-Tests?

Diese Frage muss man sich am Anfang stellen, denn es gibt viele Argumente gegen UI-Tests. Sie sind langsamer in der Ausführung, die Automatisierung ist aufwendig und nach langem Entwicklungsaufwand dann so fragil, dass man sich an fehlschlagende Tests gewöhnt und vor dem Problem des gut bekannten Broken Windows Effect [16] steht. Doch gleichzeitig ist die UI das Gesicht einer Software. Wenn die UI schlecht ist, schwer zu bedienen ist oder gar Fehler hat, so überträgt sich diese Wahrnehmung stets auch auf das Gesamtprodukt. Auch wenn dahinter oftmals komplexe Systeme stecken, die weitaus Schwierigeres zu meistern haben, wie das Bestehen unter hoher Last. UIs sind oft auch der höchste Integrationspunkt in einem Produkt. Sie verbinden evtl. viele dahinterliegende Systeme zu einer Gesamtansicht. Wenn das Gesamtsystem Fehler hat, so werden diese in der UI sprichwörtlich am augenscheinlichsten. Also der richtige Einstiegspunkt, um ein System auf Probleme „abzuklopfen“. Oder wie es Martin Fowler ausdrückte [17]: In particular I always argue that high-level tests are there as a second line of test defense. If you get a failure in a high-level test, not just do you have a bug in your functional code, you also have a missing unit test. Thus whenever you fix a failing end-to-end test, you should be adding unit tests, too.

Anforderungsanalyse

Die Anforderungen an den Neustart der UI-Tests und an das Framework ergaben sich aus unseren vielen unterschiedlichen Erfahrungen:

  • Einfach: Einen UI-Test zu schreiben (aber auch zu debuggen) soll so einfach sein, wie es für Unit-Tests der Fall ist.
  • Wiederverwendbar: Die Hilfsmethoden und -klassen für die Tests sollen zwischen den Teams wiederverwendbar sein.
  • Erweiterbar: Die Hilfsklassen sollen erweiterbar sein, denn auch unsere UI ist über Plugins erweiterbar. Zugriffe auf erweiterte UI-Elemente sollen also über Erweiterungen der bestehenden Zugriffe für die Basis-UI-Elemente möglich sein.
  • Wartbar: Dazu zählt unter anderem, dass die Tests direkt aus der IDE zu starten und zu debuggen sind. Doch dies ist nur ein ganz kleiner Ausschnitt aus den vielen Anforderungen an die Wartbarkeit.
  • Schnell: Natürlich müssen UI-Tests schnell sein, denn wir wollen schnell in der Entwicklung sein.

Mein Name ist Studio …

Zum weiteren Verständnis unseres Testansatzes muss ich zunächst einmal jemanden näher vorstellen: unser CoreMedia Studio. Abbildung 1 zeigt es bei der Arbeit. Ein Redakteur hat gerade einen Artikel über London geschrieben. Seine Arbeitsergebnisse wurden kontinuierlich im Hintergrund auf einem Content-Server gespeichert. In einer Preview konnte er dabei stets verfolgen, wie sein Artikel später auf der Webseite aussehen wird. Nachdem er per Drag & Drop noch ein Bild von seiner Festplatte hinzugefügt hat, ist der Artikel fertig und kann auf der Webseite erscheinen. Ein Schritt, den wir Publikation nennen. Für den Redakteur ein einfacher Klick in der UI. Für das System dahinter ein komplexer Vorgang von Konsistenzüberprüfungen bis hin zum Transferieren der Daten auf einen anderen Server, den sogenannten Live-Server. Und schließlich: Feedback an den Redakteur mit einer kleinen Nachricht (unten rechts).

Dieses Studio ist für die hier im Fokus stehenden Tests unsere Software Under Test, kurz: SUT. Das Studio läuft im Browser und ist eine Rich Web Application. Für unsere Kunden muss sie sich wie eine Desktop-Anwendung anfühlen. Das heißt insbesondere, dass unsere Kunden ein schnelles Antwortzeitverhalten erwarten. Ein Grund, warum wir bei der Implementierung auf AJAX gesetzt haben.

Einfache Komplexität

Einfache UI-Tests? Leichter gesagt als getan. Das fängt schon bei unserer komplexen Systemlandschaft an. Ein paar der Kommunikationswege in unserer UI zeigt Abbildung 2. Ein UI-Test muss, um schnell zu sein, sich in großen Teilen dieser Landschaft bewegen können. Also bereits bei der Testvorbereitung gilt es Komplexität vor dem Entwickler zu verbergen. Und das zieht sich weiter über die Testdurchführung und den Umgang mit asynchronen Events bis hin zum Abräumen von Testartefakten am Ende eines Tests.

Wrapper und Fassaden

Wer Komplexität verstecken will, kommt schnell auf Entwurfsmuster wie Wrapper und Fassaden. Und eben diese machen wir uns auf allen Ebenen unserer UI-Tests zunutze. Sie kapseln Zugriffe auf Backend-Systeme, auf das der UI zugrunde liegende Modell im Browser (MVC Remoting) und die UI-Elemente selbst. Die Wrapper und Fassaden werden von den Entwicklern geschrieben, die auch die entsprechenden Komponenten entwickeln. Dies hat mehrere Vorteile:

  • Sollte eine API im Produkt fehlen, um den Tests Zugriff zu geben, so können die Entwickler diese einfach einbauen.
  • In die Klassen kann Expertenwissen einfließen. Wie zum Beispiel, dass man beim Warten, ob ein Dokument (wie der Artikel unseres Redakteurs) indiziert ist, nicht jede Millisekunde nach dem Status fragen sollte, weil die Suchmaschine sonst nur mit der Beantwortung der Anfragen beschäftigt ist, statt weiter zu indizieren.
  • Die Wrapper und Fassaden können als Test-API anderen Teams zur Verfügung gestellt werden.

@Before

Die Arbeit unserer Fassaden beginnt bei der Testvorbereitung. Um die Bearbeitung eines Artikels zu testen, muss dieser erst einmal im System angelegt werden. Ein Anlegen des Artikels über die UI kommt aus mehreren Gründen nicht in Betracht:

  • Die UI ist im Anlegen des Dokuments langsam. Die ersten bei uns entwickelten UI-Tests für ein auf dem Desktop laufendes Redaktionswerkzeug verfolgten diesen Ansatz. Der Vorteil war die automatische Testabdeckung dieser Bedienungspfade, ein Nachteil jedoch, dass die Erzeugung sehr langsam war.
  • Ein Fehler im Anlegen von Dokumenten würde in der Folge alle Tests scheitern lassen – auch wenn dies gar nicht im Fokus des Tests steht. Auch diese Beobachtung konnten wir mit unseren alten UI-Tests machen: Eine Änderung der UI zum Anlegen von Dokumenten führte zum schlagartigen Ausfall aller UI-Tests.
  • Die Interaktion mit der UI ist (und bleibt) immer ein Drahtseilakt. Allein schon, weil die UI ständigen Veränderungen im Rahmen der Entwicklung unterworfen ist. Daher sollte der Zugriff auf die UI so kurz wie möglich bleiben.

Für den Entwickler sieht das Anlegen eines Dokuments über eine Fassade im einfachsten Fall wie folgt aus:

Content c = contentUtils.createDocument();

Was sich dahinter aber versteckt, ist vielfältig. Hier ein kleiner Ausschnitt:

  • Ein Dokument muss von einem Benutzer angelegt werden. Dieser wird, falls noch nicht vorhanden, angelegt.
  • Ein Dokument muss einen Typ (zum Beispiel „Artikel“ oder „Bild“) haben. Wenn, wie hier, kein Typ benannt ist, muss dennoch ein gültiger Typ ermittelt werden.
  • Das angelegte Dokument darf nicht mit schon existierenden im System kollidieren. Dazu werden Dokumente in Verzeichnisse abgelegt, die allein dem Test gehören, und die Dokumente erhalten Namen, die innerhalb eines Testlaufs einmalig sind, so dass ein erneuter Aufruf von createDocument() auch ohne Konflikte ein neues Dokument erzeugt.

Für bestimmte Aktionen muss das Dokument auch noch im Index der Suchmaschine erscheinen. Der folgende Aufruf stellt dies sicher:

searchUtils.indexed(c)
.waitUntilTrue();

Da alle Tests im Studio stattfinden, sollte dies vor dem Test auch noch in einem Browser-Fenster geöffnet werden:

studio.get();

Und bei der Ausführung passiert unter anderem Folgendes:

  • Ein gültiger Benutzer wird angelegt. In diesem Fall wird der gleiche Benutzer verwendet, der auch zuvor das Dokument über die Content-Server-API angelegt hat.
  • Ein Ping zur URL der Studio Webapp stellt sicher, dass diese überhaupt gestartet ist. Dies ist ein erster unserer vielen Schritte, um Tests so schnell als möglich scheitern zu lassen, wenn erkennbar ist, dass der Test nicht erfolgreich sein kann (Fail-fastAnsatz).
  • Ein Browser wird gestartet, so wie es der Entwickler vorher extern definiert hat, zum Beispiel Firefox Version 24.
  • Die Studio-URL wird im Browser aufgerufen und der Benutzer wird automatisch eingeloggt.
  • Sobald die Arbeitsoberfläche des Studios vollständig sichtbar ist, kehrt der Aufruf zurück.

Drei Zeilen – und schon ist das System fertig für den Test.

@Test

Nun wird es Zeit, mit der UI zu interagieren. Zu testen ist die UI in Abbildung 3. Sie bietet Zugriff auf das gerade angelegte Dokument. Wir möchten den Titel bearbeiten und testen, dass die Änderung auf dem Content-Server ankommt. Der Test sieht dann in etwa wie folgt aus:

studio.openDocument(c);
Field f = documentView.
getField(„title“);
f.sendKeys(…);
documentView.save();
contentUtils.stringProperty(c, „title“)
.assertEquals(…);

Im Idealfall, das heißt, wenn die nötige API im Testframework schon vorhanden ist, ist dies alles, was der Entwickler zu tun hat. Die UIWrapper verstecken dabei, wie die Elemente in der UI gefunden werden.

@After

Nach einem Test muss aufgeräumt werden, zumindest wenn, wie bei unseren Tests, nicht für jeden Test ein ganzes System neu gestartet oder initialisiert wird. Diese Option stellt sich für uns nicht, da das Starten des Gesamtsystems zu lange dauern würde. Für diesen Test bedeutet dies:

  • Browser beenden
  • Dokument und Verzeichnisse löschen
  • Benutzer löschen

Der Entwickler muss dies mit keiner Zeile Code im Test erwähnen. Das Wissen, welche Aufräumaktionen am Ende zu erledigen sind, wurden bereits bei der Testvorbereitung gesammelt und werden nun automatisch über das Mittel der JUnit Rules ausgeführt.

Fokus: UI-Wrapper

Die UI-Wrapper stehen im Fokus der Bemühung um Vereinfachung. Zum einen ist das Ansprechen von UIs aus Tests heraus für unsere Entwickler unbekanntes Terrain – und zum anderen ist die UI permanenten Änderungen unterworfen. Bereits in meinem Artikel Boon and Bane of GUI Test Automation [5] wies ich 2008 auf die wichtige Trennung zwischen Test und Zugriff auf die UI hin. Nur diese Trennung ermöglicht es, dass Änderungen an einem UI-Element auch nur an einer Stelle in den Tests durchgeführt werden müssen – nämlich in dem entsprechenden Wrapper.

Ein gutes Beispiel ist folgende Zeile aus unserem Test:

documentView.save();

In der Beispiel-UI aus Abbildung 3 ist erkennbar, was dies bedeutet: Ein Knopf zum Speichern ist zu drücken. Unsere Interaction Designer wünschen sich aber, dass dies der Redakteur zukünftig nicht mehr explizit tun muss. Stattdessen soll das Speichern des Dokuments stattfinden, sobald der Fokus das Textfeld verlässt.

Der konkrete Test für das Speichern muss natürlich geändert werden. Aber für diesen Test, für den das Wie des Speicherns nicht entscheidend ist, reicht es aus, wenn in dem DocumentView-UI-Wrapper die Methode save() angepasst wird. Unsichtbar für diesen und viele weitere Tests. Das Schreiben im Textfeld ist ein Beispiel für versteckte Komplexität:

f.sendKeys(…);

Für den Test ein einfacher Aufruf. In der Ausführung führt dieser Aufruf zu:

  • Content-Tab öffnen
  • Titel-Feld in den sichtbaren Bereich des Browsers rücken ó Feld fokussieren
  • einzelne Tastendrücke an das Feld senden

Wiederverwendbare Komplexität

Wenn es gilt, komplexe Schritte vor den Tests zu verstecken, so ist offensichtlich, dass diese Komplexität nur verschoben ist. Wenn der Testentwickler sich nicht um die Komplexität kümmern muss, so muss es doch der Entwickler des UI-Wrappers tun. Unser Argument, warum sich der Aufwand lohnt: Diese Wrapper sind in vielen Tests selbst über Teamgrenzen hinweg wiederverwendbar.

Um die Kosten für die Erstellung dieser Wrapper zu reduzieren, haben wir uns einen Ansatz von Selenium WebDriver [2] abgeschaut: die PageObjects [6] und insbesondere die Annotation @FindBy.

Abbildung 4 zeigt, durch welche UI-Elemente der Entwickler eines Wrappers zum Beispiel für das Textfeld navigieren muss. Wir gehen davon aus, dass der Entwickler, der den Wrapper schreibt, auch das UI-Element in der Anwendung platziert hat. Er verfügt also grundsätzlich über das Wissen, wie dieses Textfeld angesprochen werden kann.

Was der Entwickler nicht weiß, ist, in welcher genauen HTML-DOM-Struktur das Textfeld zu finden ist. Der Grund: Wir nutzen ein komponentenbasiertes, in JavaScript geschriebenes Framework namens ExtJS [7] zum Erstellen der UI. Wie die HTML-DOM-Struktur später genau aussieht, liegt ganz in der Hand von ExtJS und nicht in der Hand des Entwicklers.

Wir nehmen an, ein anderer Entwickler hat bereits einen Wrapper für den Content-Tab erstellt. Dieser kann über eine eindeutige ID gefunden werden:

@ExtJsObject(id = „contentTab“)
class ContentTab {

}

Innerhalb von Komponenten können ItemIDs vergeben werden. Diese sind nur innerhalb einer Komponente eindeutig. Um das Textfeld zu finden, ist jetzt der ContentTabWrapper nur wie folgt zu erweitern:

@ExtJsObject(id = „contentTab“)
class ContentTab {
@FindByExtJs(itemId = „titleField“)
Textfield titleField;
}

Auch hier ist der Entwickler noch im Bereich von Java. Tatsächlich sieht fast kein Entwickler beim Schreiben von Tests auch nur eine Zeile JavaScript. Ein Vorteil für uns, denn Java-Entwicklung ist für unsere Entwickler ein Heimspiel. Die Übersetzung in JavaScript geschieht erst bei der Interpretation der Annotationen zur Laufzeit. Dafür gibt es eine zentrale Implementierung, der Kern unseres UI-Testframeworks , der inzwischen so stark gereift ist, dass dort seit langer Zeit keine Änderungen mehr nötig waren. Dabei ist der Zugriff über IDs noch am einfachsten. Der folgende JavaScript-Ausdruck ermittelt über die ExtJS-API den Content-Tab:

Ext.getCmp(„contentTab“);

Die Suche über itemId war früher auch ein Einzeiler. Doch dann war ein Button über itemId anders zu finden als etwa ein Textfeld. Der JavaScript-Code wuchs auf heute fünf Zeilen Code. Der Testentwickler bekommt diese (im Idealfall) nie zu Gesicht. Nur im Fehlerfall wird der JavaScript-Ausdruck zur besseren Fehleranalyse ausgegeben.

Erweiterbar

Ich hatte zuvor beschrieben, welche Schritte notwendig sind, um in das obige Textfeld zu schreiben. Unter anderem muss das Feld fokussiert werden. Faktisch jedes Element in der UI kann fokussiert werden. ExtJS drückt dies durch Vererbung aus. Die Basis bildet eine JavaScript-Klasse Component. Von dieser erbt die Klasse Field und von dieser wiederum Textfield.

Wir haben uns diese Vererbungsstruktur zunutze gemacht und diese auch in den UIWrappern abgebildet. Abbildung 5 zeigt in einer 2-in-1-Grafik, was das bedeutet: Wenn ein Entwickler die Vererbungsstruktur von ExtJS kennt, so kennt er automatisch auch die Vererbungsstruktur der UI-Wrapper.

Ein Component-Objekt wird durch einen einfachen Klick fokussiert. Ein TextField muss vor dem Klick eventuell noch auf die Eintragung eines Default-Wertes warten, weil sonst beide Ereignisse sich gegenseitig behindern. Ein komplexeres Eingabefeld besteht vielleicht aus noch mehr Komponenten wie einer Toolbar und dem eigentlichen Eingabefeld. Hier muss der Fokus auf eben dieses Eingabefeld umgebogen werden.
Von all diesen Anpassungen, die teilweise auch der Teststabilisierung dienen, merkt der Testentwickler nichts. Und ein Überschreiben der focus()-Methode aus der ComponentKlasse ist andererseits nur nötig, falls solche Sonderbehandlungen erforderlich sind.

Wartbar

Die Wartbarkeit ist gerade im Bereich der UITests ein wichtiger und vielschichtiger Begriff. Bei Unit-Tests kommt die Wartbarkeit meist gefühlt umsonst daher. Bei Refactorings ändert die IDE auch automatisch die Tests entsprechend – und selbst, wenn wir die Implementierung ändern, so zeigen Unit-Tests sehr klar und fokussiert auf das Problem. Um es mit standardisierter Sprache der QA auszudrücken: Die beobachtete Fehlerwirkung ist sehr nah an dem eigentlichen Fehlerzustand/ Defekt. Ob der Fehler im Test liegt, der an die neuen Anforderungen angepasst werden muss, oder doch im Produktionscode, ist somit schnell entschieden.

Bei Integrations- und ganz speziell bei UI-Tests ist dies meist nicht der Fall. Als Fehlerwirkung beobachten wir etwa, dass das Speichern eines Dokuments in der UI sich nicht in einer Änderung auf dem Server niederschlägt. Die möglichen Fehlerzustände sind vielschichtig:

1. Der Content-Server nimmt keine Änderungen mehr an,

2. die Verbindung Content-Server zum Studio ist zusammengebrochen,

3. ein unerwarteter modaler Dialog im Browser verhindert, dass Klicks in der UI ankommen,

4. ein unerwarteter modaler Dialog im UITestsystem verhindert, dass Klicks in der UI ankommen, oder

5. der Speichern-Knopf wurde entfernt – eine bewusst durchgeführte Änderung der UI.

Punkt 1 ist nicht wirklich ein Problem des UI-Tests und hätte zum Beispiel von Integrationstests schon gefunden werden sollen. Punkt 3 könnte eine Fehlermeldung sein, die auch andere Tests beeinflusst, also nicht fokussiert auf diesen Testfall ist. Punkt 4 ist einer der teuersten und ärgerlichsten, denn es sind Testinfrastruktur-Probleme, die nichts mit irgendwelchen Produktmängeln zu tun haben. Punkt 5 ist der vielleicht häufigste Fall von Fehlschlägen bei UI-Tests: Die UI wurde bewusst verändert – nur die Tests wurden nicht entsprechend angepasst.

Ein Framework muss den Entwickler bei all diesen Fehlerwirkungen ausreichend informieren, damit dieser den Fehlerzustand möglichst schnell eingrenzen kann. Änderungen akzeptieren Wenn Änderungen so häufig sind, dann sollte sich dies auch im Framework niederschlagen. Die UI-Wrapper bieten hier einen einfachen Weg, weil nur ein Wrapper anzupassen ist und alle Tests laufen wieder. Vorimplementierte Suchmuster über IDs und Item-IDs nutzen zudem gängige Verfahren in UI-Tests, um beispielsweise unabhängig von Lokalisierungen oder genauen Komponentenstrukturen zu sein. Erwarte das Unerwartete Unerwartete Dialoge wie Systemfehlermeldungen sind keine Seltenheit. Oft lassen sich diese für den Entwickler lokal nicht reproduzieren. Und manchmal ist dies nicht nur ein Problem des Ortes – sondern auch der Zeit. Ein nächtliches System-Backup könnte etwa Abläufe gestört haben. Darum bieten wir unseren Entwicklern klare Berichte über die beobachteten Fehlerwirkungen. In einer Log-Datei werden beim Scheitern eines Tests unter anderem aufgeführt:

ó der genaue Zeitpunkt,

ó der evaluierte JavaScript-Ausdruck,

ó das erwartete Ergebnis,

ó das (zuletzt1) erzielte Ergebnis des Ausdrucks und

ó ein Verweis auf eine Screenshot-Datei, die sich neben dem Log befindet.

Aber der Entwickler findet noch mehr vor: Im Log steht eine detaillierte Schritt-für-SchrittAnleitung, wie der Fehlerfall manuell zu reproduzieren ist: Stories und Szenarien Ich erwähnte bereits die unterschiedlichen Reiserouten im Bereich der UI-Tests, die unsere Teams beschritten hatten. Für das Framework waren die Erfahrungen aus den Teams ein Quell der Inspiration. Ein paar unserer UI-Tests waren in Form von Stories und Szenarien geschrieben, wie es Dan North 2007 in seinem Blogpost What’s in a Story? [8] vorstellte. Interpretiert wurden diese Stories von JBehave [3]. JBehave trennt die in Prosa formulierte Story als Textdatei von dem Ausführungscode, den Steps, die in Java imple1 siehe Das Warte-Pattern

mentiert sind. Einfach formuliert liest JBehave die Textdatei und sucht per String-Matching eine passende Methode in den Java-Klassen, die dann aufgerufen wird. Das Charmante an diesem Ansatz ist, dass man automatisch gut dokumentierte Tests hat, die sich im Notfall (sei es, weil das Release drängt oder beim Debuggen) auch manuell problemlos ausführen lassen. Und: Im Fehlerfall ist im Log genau zu erkennen, welcher Arbeitsschritt gescheitert ist. Trotzdem kam für uns diese Lösung nicht in Betracht, denn:

ó die Wartung von Stories ist komplex und zumindest von unserer IDE wenig unterstützt; doch auch Stories sind einem zeitlichen Wandel unterworfen: Neue Anforderungen verändern alte Stories oder machen sie gar überflüssig,

ó die Wartung der Steps ist ebenso schwierig, denn unter anderem kann eine IDE meist nur schwerlich erkennen, ob ein Step noch in irgendeiner Story verwendet wird, und

ó nicht zuletzt gibt es eine erhöhte Lernkurve, die, wenn man die Steps auch als wiederverwendbare API begreift, kaum zu meistern ist.

Unser Ansatz holt die Entwickler wieder in ihre gewohnte Umgebung zurück – mit vollem Refactoring-Support und Usage-Analysen durch die IDE. Eine Story, die in JBehave etwa so aussehen würde:

Scenario: Document Update Given I open Document D When I save Document D Then Document D is updated
… sieht bei uns in etwa so aus:
@Test scenario_document_update() {   Reference<Content> D = ref();   given_I_open_Document_D(D);   when_I_save_Document_D(D);   then_Document_D_is_updated(D); }
Der wesentliche Unterschied ist zunächst: Unsere Tests verlassen die Java-Welt nicht. Die IDE unterstützt uns vollständig beim Umbenennen von Steps oder beim Erkennen von Step-Implementierungen, die nicht mehr genutzt werden. Und per Auto-Vervollständigung ist auch die API der Steps leicht zu lernen.

Ein kleiner Kunstgriff ist die Klasse Reference. Sie ist ein leichtgewichtiger Container für Informationen, die zwischen den StepImplementierungen transportiert werden müssen. Globale Variablen werden überflüssig, Abläufe sind leichter nachzuvollziehen, und Steps können sogar in Hilfsklassen ausgelagert werden. So gut der Ansatz mit den Hilfsklassen klingt – für uns hat er sich nicht bewährt. Es zeigt sich, dass es nur sehr wenige Steps gibt, die wiederverwendbar sind, und dass der Titel des Steps nicht immer ausreichend ist, um dessen volle Auswirkung zu verstehen. So mag etwa der Step given_I_open_ Document_D() so implementiert sein, dass vor dem Öffnen von Dokument D zunächst alle eventuell noch offenen Dokumente geschlossen werden, weil es in einem Test so benötigt wird. Ein neuer Test möchte aber mit mehreren geöffneten Dokumenten arbeiten und scheitert unerwartet wegen des versteckten Verhaltens. Stattdessen gilt für uns die Regel:

Step-Implementierungen nur lokal in der Testklasse definieren, dafür eine so reiche API im Testframework schaffen, dass die Implementierung der Steps nur aus wenigen Zeilen besteht. Beim Schreiben der Szenarien schafft dies maximale Freiheit, ohne sich verbiegen zu müssen, um eventuell doch irgendeine StepDefinition wiederverwenden zu können. Das Loggen der Szenarien und Steps haben wir mittels Aspektorientierter Programmierung [9] umgesetzt, so dass etwa in der Konsole zu lesen ist:
SCENARIO: Update Document given I open Document D (D=<none>) when I save Document D (D=content:id:1) then Document D is updated (D=content:id:1) SCENARIO: Update Document (Success)
Und im Fehlerfall etwa:
SCENARIO: Update Document given I open Document D (D=<none>) given I open Document D (Failed) SCENARIO: Update Document (Failed)
Die Referenzen werden zum leichteren Debuggen mit ausgegeben. Initial sind sie leer, daher <none>. Zusammen mit Logs, Screenshot und den Schritten bis zum Scheitern können Entwickler schon bei der ersten Analyse sehr genau ermitteln, was fehlgeschlagen sein könnte, und mit wenig Aufwand das Szenario manuell durchspielen.
Geduldige Schnelle UIs mit gutem Antwortzeitverhalten, wie unser Studio, arbeiten asynchron. Wenn der Redakteur zu Beginn den London-Artikel publiziert, so wird er irgendwann über den Erfolg der Aktion informiert werden. „Irgendwann“ ist für Tests ein Problem, welches bei uns schnell mit dem Schlagwort Asynchronitätshölle betitelt wurde. Für das Warten auf Ereignisse, deren Eintreten nicht vorhergesehen werden kann, kennt der Java-Entwickler einen vielgenutzten Aufruf:
Thread.sleep(1000L);
Dies legt die Testausführung für 1000 ms schlafen. Wenn das noch nicht gereicht hat, wird der Aufruf vielleicht noch in eine Schleife eingebettet:
while (!dialog.isVisible()) {   Thread.sleep(1000L); }
Das offensichtlichste Problem: Im Fehlerfall endet dieser Aufruf nie. Also wird in die whileSchleife noch die bereits vergangene Zeit mit einbezogen. Dieses Pattern fanden wir vor der Framework-Entwicklung mehrfach in den Tests in verschiedensten Ausprägungen. Der grundsätzliche Ansatz ist richtig, wie auch Matt Wynne in Death to sleeps! [10] schrieb: Wir brauchen eine Form von Polling, die die SUT bis zu einem Timeout regelmäßig überprüft. Für diesen Ansatz entstand bei uns: Das Warte-Pattern Wenn wir auf ein externes System, wie es für uns auch die UI ist, warten müssen, so ergeben sich drei Rollen in einem Warte-Pattern: ó der SUT-Delegierte, der in der Lage ist, einen Zustand in der SUT abzufragen, ó der Überprüfer, der einen Ist-SUTZustand mit einem Soll-SUT-Zustand vergleicht, und ó der Überwacher und Ausführer, der den SUT-Delegierten so lange nach dem SUTZustand fragt, bis eines der folgenden Ereignisse eintritt: ó Der Überprüfer meldet, dass der SollZustand erreicht ist, ó dass eine vorgegebene maximale Wartezeit erreicht ist, oder ó der SUT-Delegierte meldet, dass ein Fehler aufgetreten ist, der ein Erreichen des Soll-Zustandes verhindert.
Diese drei Rollen tragen bei uns die Namen: ó der SUT-Delegierte: Expression ó der Überprüfer: Matcher ó der Überwacher: Condition
Das Zusammenspiel stellt Abbildung 6 dar. Die Namen der drei Rolleninhaber rühren von ihren Ursprüngen her: ó Expression: kapselte ursprünglich einen JavaScript-Ausdruck; inzwischen kann sie aber jedes beliebige System abfragen, wie zum Beispiel auch unseren ContentServer ó Matcher: JUnit selbst setzt inzwischen intensiv auf Hamcrest Matcher [11] für einen Soll-Ist-Vergleich. Der Vorteil: Es gibt bereits eine reichhaltige Bibliothek von Vergleichen (größer als, enthält, …), und die API ist sehr leicht um eigene Matcher erweiterbar. ó Condition: benannt nach der Klasse ExpectedCondition [12] in Selenium WebDriver, das Warte-Pattern, welches Selenium für das Warten vor allem auf den Zustand von HTML-DOM-Elementen zur Verfügung stellt

Der Vorteil des Warte-Patterns: Ein Entwickler muss je nach Rolle nur über wenig Wissen verfügen:

  • Expression-Entwickler: muss als Einziger wissen, wie man auf die SUT zugreift. Eine klar definierte API regelt die möglichen Zustände, die eine Expression annehmen kann. ó Testentwickler: verwendet die Expression, besorgt sich über eine Factory eine zum Rückgabewert der Expression passende Condition und wählt über einen Matcher den gewünschten Soll-Zustand aus
  • Testausführer: gibt von außen das Antwortzeitverhalten der SUT vor. In einem nächtlich ausgeführten Test darf eventuell schon mal 60 Sekunden auf den Soll-Zustand gewartet werden, beim Debugging möchte er aber den Test bereits nach 10 Sekunden abbrechen.

Unsere UI-Wrapper liefern dabei meist schon die Expression verpackt in eine passende Condition, so dass für den Tester das Warten zum Beispiel so aussieht:
feature.enabled() .assumeTrue(); button.enabled() .waitUntilTrue(); textField.value() .assertEquals(„London“); integerField.value() .assertThat(greaterThan(5)); Das heißt, unsere Conditions haben eine vergleichbare API zu JUnit und die gleichen Endzustände, die man auch von JUnit kennt: ó Erfolg: erfolgreiche Assertion ó Misserfolg: misslungene Assertion ó Fehler: Exception bei der Testausführung, bei uns ausgelöst durch waitUntilMethoden ó Ignorieren: Test nicht valide in diesem Kontext, das heißt, die Annahme ist falsch und die Testaussage insofern nicht von Belang (assume)
Verlustreiche Eile Das Warte-Pattern ist beliebig geduldig. Aber wie bleiben unsere Tests trotzdem schnell? Wir können das Polling-Intervall einfach verringern. Dann stoßen wir aber in eingangs erwähnte Probleme, dass die SUT mehr mit dem Beantworten der Anfragen beschäftigt ist als damit, ihren Zielzustand zu erreichen. Olaf Kummer beschrieb dieses Problem und unsere Lösung 2012 in seinem Blogbeitrag
SUT
Haste makes waste [13]. Im Wesentlichen ist die Lösung so zu beschreiben:
Wir beginnen mit einer hohen Polling-Frequenz. Ist die Anfrage nicht erfolgreich, so verringern wir sie langsam, aber stetig und können dabei sogar das Antwortzeitverhalten eines Polls mit berücksichtigen. Am Ziel … Nach einer langen Reise unserer Teams mit unterschiedlichen Reiserouten haben wir es geschafft, alle zu vereinen. Entwickler berichten mir, wie positiv die Erfahrungen beim Schreiben von UI-Tests sind, und wünschen sich einen ähnlichen Komfort in der Testentwicklung auf den anderen Ebenen der Testpyramide [14]. Erste Überlegungen gehen dahin, einzelne UI-Komponenten auch außerhalb des Studio-Kontextes testen zu können, also auf einem niedrigeren Integrationslevel, für die Zugriffe aber die UI-Wrapper wiederzuverwenden. Die Selenium-Gruppe trifft sich immer noch regelmäßig und ist inzwischen beispielgebend für viele weitere Communities of Practice, die bei uns gebildet werden. Am Ziel sind wir aber noch lange nicht. Bis zuletzt haben uns Probleme mit der SeleniumInfrastruktur begleitet – ein vielversprechender Lösungsansatz ist wiederum ein Ergebnis der gesammelten Erfahrung aus allen Teams. Die Nagelprobe muss dieser Ansatz aber erst noch bestehen. Ein Lohn der Mühen zeichnet sich auch bereits ab: Eine erste Evaluation eines Major Updates von ExtJS, welches grundlegende Änderungen einführt, wird unsere Wrapper nur minimal beeinflussen – und die Tests werden völlig unberührt bleiben.
Warum automatische Tests? Wenn UI-Tests schwer zu schreiben und zu pflegen sind, lohnt sich dann eine Automatisierung? Schon 2008 verwies ich in meinem Beitrag Boon and Bane of GUI Test Automation [5] auf Alexandre (Shura) Ilines Automation Effectiveness Formula [15]. Wieder hervorgeholt, birgt sie immer noch wertvolle Gesichtspunkte, die man für die Testautomatisierung zu betrachten hat:

ó Entwicklung: Wie lange dauert die Entwicklung eines automatischen Tests?

ó Wartung: Wie lange dauert die Pflege eines automatischen Tests?
ó Manuell: Wie lange dauert die manuelle Durchführung eines Tests?

ó Anzahl Plattformen: Auf wie vielen Plattformen wird ein Produkt verwendet bzw. muss es getestet werden?

 

Bereits die Parameter der Formel machen eines deutlich: Unter gewissen Umständen können manuelle Tests effektiver sein. Sobald es aber um eine ständige Wiederholung, eventuell gar tägliche Releases geht, lohnt sich die Investition in die Automatisierung – solange die anschließende Wartung ausreichend gering gehalten werden kann.
UI
Manual
Service
Unit
Wichtig ist aber, auf die richtige Ebene der Testautomatisierung zu achten. Mike Cohn illustriert dies in der Testpyramide [14]. Je höher ein Test in der Pyramide steht, desto mehr Aufwand sind die Pflege und Entwicklung und desto weiter sind unter Umständen beobachtete Fehlerwirkung und der ursächliche Fehlerzustand voneinander entfernt. Daher sollte die Basis der Unit-Tests breit sein. UI-Tests stehen an der Spitze von Mike Cohns Pyramide – in einigen ergänzten Modellen werden auch noch die manuellen Tests als diffuse Wolke (wegen des explorativen Ansatzes) aufgeführt.

Verweise

[1] „JUnit“ [Online]. Available: http://junit. org/. [Zugriff am 09.11.2013].

[2] seleniumhq.org, „Selenium WebDriver“ [Online]. Available: http://www.seleniumhq.org/. [Zugriff am 03.11.2013].

[3] „JBehave“, 11.07.2012. [Online]. Available: http://jbehave.org/. [Zugriff am 03.11.2013].

[4] M. Cohn, „Cultivate Communities of Practice“, 23.11.2009. [Online]. Available: http://www.mountaingoatsoftware. com/blog/cultivate-communities-ofpractice. [Zugriff am 12.11.2013].

[5] M. Michaelis, „Boon and Bane of GUI Test Automation“, Testing Experience, Nr. 4/2008, p. 25 ff., Dezember 2008.

[6] seleniumhq.org, „Selenium Wiki, PageFactory“ [Online]. Available: https:// code.google.com/p/selenium/wiki/PageFactory. [Zugriff am 03.11.2013].

[7] Sencha Inc., „ExtJS“ [Online]. Available: http://www.sencha.com/products/extjs. [Zugriff am 03.11.2013].

[8] D. North, „What’s in a Story?“, 11.02.2007. [Online]. Available: http://dannorth.net/ whats-in-a-story/. [Zugriff am 12.11.2013].

[9] G. Kiczales, J. Hugunin, E. Hilsdale, M. Kersten, J. Palm, C. Lopes, B. Griswold und W. Isberg, „Aspect Oriented Programming“, Springer, Berlin Heidelberg, 1997.

[10] M. Wynne, „Tea-Driven Development – Death to sleeps!“, 03.06.2013. [Online]. Available: http://blog.mattwynne. net/2013/06/03/death-to-sleeps/. [Zugriff am 03.11.2013].

[11] „Hamcrest Matcher“ [Online]. Available: http://hamcrest.org/. [Zugriff am 09.11.2013]. [12] seleniumhq.org, „Selenium WebDriver: ExpectedCondition API-documentation“ [Online]. Available: http://selenium. googlecode.com/git/docs/api/java/org/ openqa/selenium/support/ui/ExpectedCondition.html. [Zugriff am 09.11.2013].

[13] O. Kummer, „Haste makes waste“, CoreMedia AG, 29.11.2012. [Online]. Available: http://minds.coremedia.com/2012/11/29/ haste-makes-waste/. [Zugriff am 09.11.2013].

[14] M. Cohn, „The Forgotten Layer of the Test Automation Pyramid“, 17.12.2009. [Online]. Available: http://www. mountaingoatsoftware.com/blog/theforgotten-layer-of-the-test-automationpyramid. [Zugriff am 09.11.2013].

[15] A. Iline, „Automation Effectiveness Formula“, 2006. [Online]. Available: https:// jemmy.java.net/AutomationEffectiveness.html. [Zugriff am 03.11.2013].

[16] Wikipedia, Deutsch, „Broken-WindowsTheorie“ [Online]. Available: http:// de.wikipedia.org/wiki/Broken-WindowsTheorie. [Zugriff am 03.11.2013].

[17] M. Fowler, „TestPyramid“, 01.05.2012. [Online]. Available: http://martinfowler. com/bliki/TestPyramid.html. [Zugriff am 10.11.2013].

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Kategorien

Recent Posts