Einführung von automatisierten Tests - Teil 2
04.08.2016
Nachdem sich der Start der Mission “Automatisiertes Testen” mit dem Framework Geb zunächst sehr vielversprechend darstellte, zogen mit zunehmender Komplexität am Horizont die ersten dunklen Wolken auf. (Im ersten Teil dieser Artikelserie wird beschrieben, wie wir dazu gekommen sich, Geb als Framework auszuwählen, wie wir erste Tests mit dem Framework umgesetzt haben und welche Schwierigkeiten dabei auftraten.
Der erste Teil unserer Serie findet sich hier.
Multilanguage ist gar nicht groovy
Es zeigte sich schnell, dass Groovy nicht einfach zu handhaben war. Auf Grund der Paradigmen der Sprache zeigt die IDE (Idea) einerseits während des Implementierens und zur Compilezeit z.B. keine Signaturdifferenzen und Typkonflikte an. Andererseits werden zum Teil Variablen und Konstanten als fehlerhaft markiert, die auf Grund des Kontexts, in dem sie stehen, nicht fehlerhaft sind, sondern zur Laufzeit korrekte Ergebnisse liefern. Dies betrifft insbesondere die content-Elemente, die im Geb-Framework benutzt werden. Diese sind auf Grund des vorherigen Navigierens auf eine bestimmte Seite und der inhärenten Prüfung auf der richtigen Seite gelandet zu sein (-> at-Checker) sehr wohl vorhanden, werden aber nicht so interpretiert. Da in diversen Forenbeiträgen die dynamische Typisierung von Groovy allseits gelobt und als Vorteil heraus gestellt wird, beschlossen wir, unser Vorgehen beim Implementieren zu verändern und es weiter mit Groovy zu versuchen. Mit zunehmender Komplexität stellten sich schnell die ersten Fragen, die das Internet nicht hinreichend beantworten konnte. Insbesondere zum Thema Test-Wiederverwendung (z.B. zum Abprüfen mehrer I18N-Varianten) war wenig bis nichts im Internet zu finden.
Details der Implementierung
Eine der ersten Fragen, die sich bei der Entwicklung aufdrängten, war: Was machen wir mit Seiten, die Inhalte erben? Unsere Anwendung sieht z.B. einen generellen Header und Footer vor, der auf jeder Seite zur Verfügung steht. Neben diesem einfachen Fall ist die Anwendung so strukturiert, dass es links eine Navigation gibt, die jedoch nicht auf jeder Seite zu sehen ist. Ist sie ausgeblendet, geht dies zu Gunsten des Inhalts in der Mitte, der dann entsprechend breiter dargestellt wird. Rechts werden ebenfalls abhängig davon, ob ein Benutzer eingeloggt ist oder nicht, unterschiedliche Informationen und Links angezeigt. Der mittlere Teil enthält die wesentlichen Informationen und ändert sich mit jedem Klick.
Aus Sicht der Implementierung steht hier das Prinzip der Vererbung ganz oben auf der Liste. Es ist sinnvoll für diesen Fall eine Basis-Seite zu definieren, die als Module den Header, den Footer sowie weitere unveränderliche Elemente beinhaltet. Von dieser Basis-Seite erben alle weiteren. Die meisten Seiten haben trotz gleichen Headers und Footers (und ggf. weiterer einheitlicher Teile) eine eigene URL. Damit qualifizieren sie sich laut Framework-Vorgaben als PageObjects und nicht als Module. Wir wollten diese Architektur daher mit einer Vererbungshierarchie umsetzen.
Dabei haben wir auch direkt die Sprache in unseren Entwurf mit einbezogen. Da der Inhalt einer Seite immer derselbe ist, egal in welcher Sprache sie dargestellt wird, sollte er in einer übergeordneten Seite definiert werden. Sprachspezifische Abweichungen sollten in den entsprechenden Sprach-Seiten definiert werden. Diese etwas umständlich anmutende Konstruktion ist der Tatsache geschuldet, dass man bei Geb den Seiten keine Parameter übergeben, sondern diese nur mittels Vererbung konfigurieren kann. Unterschiedliche Sprachen lassen sich daher nur durch eine neue Klasse pro Seite und Sprache umsetzen. Außerdem müssen alle Übersetzungen für Labels und weitere Texte in den sprachspezifischen Seiten untergebracht werden. Unser erster Entwurf war zwar übersichtlich, legte jedoch nahe, dass mit zunehmender Anzahl Sprachen und implementierter Seiten eine Klassenexplosion auf uns zukommen würde.
Möglichkeiten und Grenzen des Geb-Frameworks
Schnell mussten wir außerdem die fundamentale Lektion lernen, dass der at-Checker je PageObject definiert werden muss. Zur Laufzeit wird je PageObject-Klasse nur der at-Checker der tatsächlichen Instanz ausgeführt. Nur wenn der at-Checker eine andere Implementierung aufweist, wird er von Geb aufgerufen. Ansonsten geht das Framework davon aus, dass man sich noch auf der ursprünglichen Seite befindet. Wenn man also einen at-Checker pro Seite (z.B. HomePage, RegisterPage) definiert funktioniert alles. Wenn man aber von HomePage_DE auf HomePage_EN wechselt, glaubt Geb, dass man noch auf der HomePage_DE Seite ist. Entsprechend stimmen die Übersetzungen nicht. Wenn man den at-Checker sogar nur in BasePage definiert, funktioniert gar kein Seitenwechsel. Unser erster Ansatz den Title der Seite per Vererbung in den einzelnen Seiten zu definieren und den at-Checker in BasePage einfach den Seitentitel prüfen zu lassen, war daher zum Scheitern verurteilt. Den Grund zu finden, wieso es nicht funktioniert, hat seine Zeit gedauert. Im Internet konnte man zu so einer konkreten Frage nichts finden. Schaut man sich den at-Checker etwas genauer an, ist die Idee dahinter durchaus sinnvoll. Der at-Checker soll nur kurz sicher stellen, dass man sich auf der Seite befindet, die man ansteuern will. Man kann dies gut mit einem Navigationssystem vergleichen, das einen zu einem eingegebenen Ort führt und am Ziel nur sicher stellt, dass man bei den korrekten Koordinaten angekommen ist. Ob da ein Haus steht und der heimische Fernseher noch in der Wohnung vorhanden ist, ist für die Feststellung am richtigen Bestimmungsort zu sein, nicht relevant. Diese Prüfung muss separat vorgenommen werden. Der Content-Bereich der PageObjects verhält sich dagegen additiv. Alle in übergeordneten Klassen definierten Content-Objekte werden bis nach unten vererbt und sind entsprechend auch zugreifbar. Da der erste Entwurf auf lange Sicht nicht funktionieren würde, mussten wir im Sinne des Frameworks refaktorisieren. Es entstand für jede (abstrakte Basis-)Seite ein at-Checker. Die sprachspezifischen Seiten haben wir in diesem Zusammenhang auf ein Minimum reduziert. Sie mussten jedoch aus dargelegten Gründen einen at-Checker bekommen. Dieser rief in unserem Fall die Implementierung der Elternklasse auf, damit der Sprachwechsel funktioniert. In Geb ist es möglich, den content-Elementen mitzugeben, ob sie optional sind, so dass alle Felder in die Basisklassen der einzelnen Seiten verlegt werden konnten. Die sprachspezifischen Seiten enthielten zum Schluss nur noch die Übersetzungen. Da die Basisseite als abstrakte Klasse implementiert war, war es möglich, den at-Checker in der Basisseite zu verankern. Gerne hätten wir die Übersetzungen als Parameter an die Basisseiten übergeben und so die sprachspezifischen Seiten vermieden. Es ist jedoch in Geb nicht vorgesehen, den Pages Parameter zu übergeben. Unser neuer Entwurf ähnelte dem alten zwar auf den ersten Blick, hatten aber an den entscheidenden Stellen die framework-konformen Unterschiede.
Listing RegisterPage (1. Version):
class RegisterPage extends BasePage {
static content = {
name { $("#registration #name") }
firstname { $("#registration #firstname") }
…
}
}
Listing RegisterPage_EN (1. Version):
class RegisterPage_EN extends RegisterPage {
static url = "/en/register/"
String TITLE = "Title of the web site"
String HEADER = "Header of the page"
String REGISTER_BUTTON_TEXT = "Register"
String[] MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
…
static content = {
special_EN_field_1 { $("#registration #special_EN_field_1") }
special_EN_field_2 { $("#registration #special_EN_field_2") }
…
}
}
Listing RegisterPage (2. Version):
class RegisterPage extends BasePage {
static at = {
title == TITLE
}
static content = {
name { $("#registration #name") }
firstname { $("#registration #firstname") }
special_EN_field_1(required: false) { $("#registration #special_EN_field_1") }
special_EN_field_2(required: false) { $("#registration #special_EN_field_2") }
…
}
}
Listing RegisterPage_EN (2. Version):
class RegisterPage_EN extends RegisterPage {
static url = "/en/register/"
String TITLE = "Title of the web site"
String HEADER = "Header of the page"
String REGISTER_BUTTON_TEXT = "Register"
String[] MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
…
}
Hinterfragung des Entwurfs
Auch mit dieser Überarbeitung bestand weiterhin das Problem, dass die Anzahl der Klassen pro Page durch die Anzahl der unterstützten Sprachen bedingt ist und schnell wachsen würde. Betrachtet man nun das vollständige Klassendiagramm, so erschließt sich dieses nicht direkt.
Der blau unterlegte Teil im Klassendiagramm repräsentiert den Testfall. Alles andere sind die notwendigen Klassen, die das Modell der Anwendung wiedergeben. Auch im Bereich des Testfalls traten leichte Bedenken auf. Auf Grund unseres Plans, jeden Test einzeln ausführen und beliebig kombinieren zu können, haben wir für jeden Fall eine eigene Klasse angelegt. Auch dies war wieder ein Schritt in Richtung Klassenexplosion. Hintergrund war, dass die Testfälle auf lange Sicht so komponiert werden können sollen, dass alle denkbaren Szenarien der Anwendung abgedeckt werden.
Listing WPageNav:
trait WPageNav {
Class<? extends HomePage> getHomePage() {
HomePage
}
Class<? extends RegisterPage> getRegisterPage() {
RegisterPage
}
}
Listing WPageNav_EN:
trait WPageNav_EN extends WPageNav {
Class<? extends HomePage> getHomePage() {
HomePage_EN
}
Class<? extends RegisterPage> getRegisterPage() {
RegisterPage_EN
}
}
All diese kleinen und großen Hindernisse führten im Endeffekt zur Hinterfragung der ersten Entscheidung für das Geb-Framework. Sicher konnten hier Vorteile gesehen werden, wie z.B. der komfortable Zugriff auf Webelemente, jedoch ließen sich auch die Nachteile nicht von der Hand weisen, wie z.B. das unkomfortable und fehleranfällige Verhalten bei der Refaktorisierung. Auch das Klassendiagramm eignet sich nur bedingt für die Einarbeitung in die Testanwendung. Als Tester ist man immer gut beraten, Dinge zu hinterfragen. Und so haben wir das, was wir auf unsere Applikation und das manuelle Testen anwenden, auf uns selbst und das Vorgehen bei der Automatisierung angewandt. Als Ergebnis entschieden wir, Geb zunächst auf Eis zu legen und ein weiteres Framework zu evaluieren. Da in unserer Firma ein gutes und breites Scala-Wissen vorliegt, lag es nahe, mit ScalaTest zu arbeiten. Erste Implementierungen von Tests mit diesem Framework gab es zudem bereits in einem anderen Projekt. Diese Tests bezogen sich zwar auf das Backend, da aber auch ScalaTest das Page Pattern vorsieht, sollte dieses Framework auf seine Tauglichkeit untersucht werden. Welche Erfahrungen wir hierbei machten, wird im dritten Teil dieses Berichts veröffentlicht. Ein ausführlicher 2-teiliger Bericht zu dieser Thematik erscheint außerdem im Javamagazin im September (11.2016) und Oktober 2016 (12.2016).