Montag, 8. August 2011

Selenium mit Groovy

Hin und wieder muss man sich einfach einmal umsehen. Zwar verwendet meine Firma für die Testautomatisierung Quick Test Professional - aber wie fühlt es sich im Vergleich dazu an, mit einem anderen System wie etwa Selenium zu arbeiten?

Schon länger benutze ich einen kleinen Teil von Selenium - die sogenannte Selenium IDE. Es handelt sich um ein auch für sich allein sehr nützliches Plugin für den Firefox.[1] Es hat eine Recorderfunktion und kann Dialogschritte im Web aufzeichnen und in Form einer XHTML-Tabelle abspeichern. Angenehm ist die Möglichkeit, über die rechte Maustaste Assertions und Verifications in den Test einzufügen. Eine ausgezeichnete kontextsensitive Hilfe unterstützt beim Einfügen von Kommandos. Und die Identifizierung von Elementen ist auf verschiedene, unter Web-Entwicklern verbreitete Weisen möglich: Natürlich kann man direkt den Elementnamen oder die ID verwenden. Aber auch CSS-Selektoren, JavaScript-DOM-Terme und XPath-Ausdrücke sind möglich.

Ich verwende die Selenium-IDE hauptsächlich, um immer gleich Sequenzen von Dialogschritten in einer Web-Anwendung abzuspielen. Das ist vor allem während des Entwickelns nötig: Wenn ich das fünfte Bild einer Anwendung entwickle, kommt mir ein Tool entgegen, das auf Knopfdruck alle Daten initialisiert und sich dann auf "die normale Weise" bis zu diesem fünften Bild durcharbeitet. So kann ich die Applikation schnell immer wieder in den gleichen Zustand bringen und mir anschauen, wie sich meine letzten Änderungen auswirken. Wenn dieses "Anschauen" - das ja ein Kontrollieren ist - sich in Form von Assertions formulieren lässt, baue ich sie gleich in den Test ein. Wenn ich dann mit dem sechsten Bild fortfahre, habe ich durch das Abspielen des Tests zugleich eine gewisse Sicherheit, dass alle vorigen Bilder weiterhin korrekt durchlaufen werden.



Wenn der Geradeausfall realisiert ist, kommen Randfälle an die Reihe, Sonderfunktionen einzelner Bilder, aber auch Tests zum Nachstellen eines Bugs.
So entsteht im Laufe des Projekts eine Halde von XHTML-Dateien mit Tests. Tests, die wirklich nicht mehr sind als eine Aufzeichnung von Dialogschritten.

Allerdings: Wiederverwendbarkeit ist auf dieser Ebene nicht möglich. Es gibt meines Wissens innerhalb der Selenium-IDE keine Möglichkeit, Tests in Tests aufzurufen oder auf irgendeine andere Weise Folgen von Selenium-Kommandos zu Modulen zusammenzufassen. Das ist auch nicht so gedacht. Der vorgesehene Ablauf ist, dass diese Aufzeichnungen nun in eine geeignete Programmiersprache exportiert werden und dort z.B. mittels eines UnitTest-Frameworks ausgeführt, zu Suiten zusammengefasst, modularisiert, parametrisiert, nachgebessert, refaktorisiert werden. Erst auf dieser Ebene hat man ein geeignetes Programmierumfeld. Dort beginnt erst das solide Arbeiten an Testcode.

Um auch diese Dimension von Selenium einmal kennenzulernen, habe ich mich dazu entschieden, einige der Tests as meinem letzten Projekt in die Programmiersprache Groovy zu exportieren und den Code dann dort nachzubearbeiten.

Groovy ist vor allem wegen seiner DSL-Features zur Formulierung von Testfällen ideal geeignet. Das gilt auch für Selenium - obwohl, wie ich feststellen musste, der Groovy-Export nur unvollständig funktioniert[2], und obwohl eine zunächst vorgesehene Hilfsklasse GroovySeleneseTestCase als Basisklasse in früheren Releases einmal vorgesehen war, aber mittlerweile nicht mehr in der Distribution enthalten ist.

Auch davon abgesehen, bietet der generierte Code nur einen groben Anhalt. Wenn man sich aber anschaut, wie die Kommandos unter Verwendung des Objekts Selenium.java abgesetzt werden können, ist es nicht besonders mühsam, die Scripts manuell zu erfassen. Auch wäre es kein Hexenwerk, einen eigenen Exporter des XHTML-Testfalls nach Groovy zu schreiben. Beispiele, wie man so etwas mit XSLT machen könnte, finden sich im "XSLT-Kochbuch" von Sal Mangano.[3]

Ich habe den GroovySeleneseTestCase.groovy wieder auferstehen lassen, aber nur mit dem Zweck, den Setup- und Teardown-Code nur einmal hinzuschreiben, mit dem der "Selenium-Server", der Vermittler zwischen dem Testscript und der zu testenden Anwendung, gestartet und heruntergefahren wird:

import org.junit.*;
import com.thoughtworks.selenium.*;

public class GroovySeleneseTestCase {
@Delegate protected SeleniumHelper seleniumHelper;
@Before
public void startSelenium() {
Selenium s = new DefaultSelenium(
"localhost",
4444,
"*chrome",
"http://test.mgb.ch/sap/bc/bsp/sap/ztm_bestellung/main.do"
);
seleniumHelper = new SeleniumHelper( s );
start()
}
@After
public void tearDown() {
stop()
}
@Test @Ignore void dummy() {}
}

Den nicht auszuführenden Dummy-Test habe ich nur eingefügt, um die Ausführung der Setup- und Teardown-Methode zu verhindern, wenn man aus irgendwelchen Gründen diese Groovy-Klasse nicht nur kompilieren, sondern ausführen will.[4] Denn diese Klasse ist nur als Oberklasse für die anderen, die eigentlichen Testklassen gedacht.

Man beachte die schöne Groovy-Annotation @Delegate. Sie wirkt so, als würde man in einem statischen Programm alle Methoden der Klasse SeleniumHelper zu eigenen erklären, also
public void start() {
seleniumHelper.start();
}
public void stop() {
seleniumHelper.stop();
}
usw.
Allerdings wirkt die Annotation @Decorate nicht statisch, sondern dynamisch – indem sie dem Trägerobjekt beim Methoden-Lookup eine Methode des Delegationsobjekts mit der gleichen Signatur anbietet.

Der SeleniumHelper.groovy wiederum ist meine Erweiterung der Selenium-Klasse. Mit demselben Trick @Decorate sind alle Methoden von Selenium.java verfügbar - darüberhinaus aber ist diese Klasse ein Pool, in den ich alle wiederkehrenden Code-Abschnitte auslagern kann, die nicht anwendungsspezifisch sind, sondern die Ausführung von Selenium-Kommandos betreffen.
class SeleniumHelper implements Selenium {  
@Delegate Selenium selenium;
SeleniumHelper(Selenium s) {
selenium = s;
}
static final timeout = "30000"
def pageLoaded = {
waitForPageToLoad(timeout);
true
}
def clickAndWait = {
click it
waitFor pageLoaded
}
public waitFor(f) {
if (f == pageLoaded)
pageLoaded()
else
for (int second = 0;; second++) {
Thread.sleep(1000);
if (second >= 60) fail("timeout");
try { if (f()) break; } catch (Exception e) {}
}
}
}

Wie in Scriptsprachen üblich, verwende ich die Datenstrukturen von Hashs und Arrays, um die Testdaten zu notieren - Dinge wie Partner-, Kontrakt-, Artikelnummern usw., wie sie für den zu testenden Bestellvorgang benötigt werden:
// Testdaten
def test = [
partner:"0010006179",
kontrakt:"0020007901",
items: [
[
matnr:"400617200000",
menge:"2",
vkhm:"8000000851",
type:"P",
lieferantPos:"0",
listPos:"0003"
],
[
matnr:"400617200000",
menge:"2",
vkhm:"8000001585",
type:"N",
lieferantPos:"1",
listPos:"0005"
]
]
]


Den eigentlichen Testfall kann ich unter Verwendung der Groovy-Sprachfeatures sehr kompakt programmieren. Hier ist er:
class StraightOrder extends GroovySeleneseTestCase {  
@Delegate OrderSteps orderSteps
@Before
void setup() {
orderSteps = new OrderSteps( seleniumHelper )
clearSessionData
}
@Test
void selectIndividualItemsAndSave() {
selectTagsWithContract partner:test.partner, kontrakt:test.kontrakt
takeToBasket test.items
selectPrintOfficeFor test.items
goTo "OrderData"
setRequestedDeliveryDateToday()
goTo "Bestellung"
assert isElementPresent("final_item_list")
click "jurinfo_gelesen"
clickAndWait "saveBestellung"
// Erfolgsmeldung nach dem Sichern:
assert getText("msg") =~ /^Ihre Bestellung .* wurde versendet!$/
}
}
Einige der hier verwendeten Methoden - wie click, isElementPresent und open gehen direkt an Selenium.java. Andere wie selectTagsWithContract sind bereits das Ergebnis einer Modularisierung, die ich in eine Delegationsklasse OrderSteps.groovy ausgelagert habe. Dort stehen sie für weitere Testklassen und -methoden zur Verfügung.

Hier der Beginn der Klasse OrderSteps.groovy mit der Implementierung von selectTagsWithContract:
class OrderSteps {
@Delegate private SeleniumHelper seleniumHelper;
OrderSteps( SeleniumHelper s) {
seleniumHelper = s;
}
def clearSessionData = {
open "/ztm/bestellung/clearBasketAndSession"
}
def selectTagsWithContract = { selected ->
open "../ztm_suche/main.do"
type "partner", selected.partner
type "kontrakt", selected.kontrakt
clickAndWait "startSelection"
}
def goTo = {
clickAndWait "css=.goto$it"
}
...
Was ist mein Fazit? Die Kombination Selenium mit Groovy, auch wenn sie vom Selenium-Team eher stiefmütterlich behandelt wird, ist vielversprechend und macht Appetit auf mehr!

Der Testcode ist ein wichtiger Teil eines Automatisierungstools. Die Entscheidung des ehemaligen Mercury-Teams, im QuickTestProfessional System ausgerechnet vbScript zugrundezulegen, zeugt von einer unangemessenen Verachtung der Code-Ebene. Auch als das Tool entstand, gab es bereits bessere Optionen. Hier ist Selenium klar im Vorteil, denn es wurde im Design auf eine Entkopplung der Ausführungsumgebung vom steuernden Code grosser Wert gelegt.

Aus Entwicklungssicht bedeutet genau diese Eigenschaft des Selenium-Systems den entscheidenden Vorteil. Für eine Gesamtbeurteilung muss aber auch das Umfeld einbezogen werden. QTP ist ab Werk mit dem Quality Center verknüpft, und damit mit einem Bugtrackingsystem, mit Planungs- und Auswertungsmöglichkeiten für ganze Entwicklungsprojekte - die Betriebsphase eingeschlossen. Ob Selenium (eventuell in Kombination mit auf Selenium aufsetzenden anderen Produkten) hier mithalten kann, weiss ich nicht.

[1] Leider ist die Selenium IDE aktuell (Stand 8.8.2011) noch nicht für Firefox 4 und höher freigegeben. Sie ist aber trotzdem auch in Firefox-Releases ab 4.0 verwendbar, indem man sich durch Installation des Add-on Compatibility Reporters zum Releasetester erklärt. Ich habe das gemacht und bislang noch keinen Fehler bei der Verwendung in FF 4 entdeckt.
[2] Beispielsweise wird ein in regulären Ausdrücken vorkommendes Dollarzeichen nicht maskiert, was zu einem Fehler in groovyc führt, der beim Dollarzeichen einen Variablennamen oder einen evaluierbaren Ausdruck erwartet.
[3] Sal Mangano, Das XSLT-Kochbuch, O'Reilly 2006.
[4] Und solche "irgendwelchen Gründe" liegen bei mir vor. Ich habe mir nämlich in UltraEdit ein Werkzeug für die Groovy-Bearbeitung eingerichtet, das erfolgreich compilierte Klassen immer auch gleich ausführt.

Keine Kommentare :