Eclipse RCP Anwendungen testen mit SWTBot
Eclipse SWTBot ermöglicht die Umsetzung robuster Oberflächentests für Eclipse RCP-Applikationen. Aus JUnit-Tests heraus können Anwendungen ferngesteuert und systematisch abgeprüft werden. Der folgende Artikel zeigt, wie Sie mit dem Werkzeug zum Erfolg gelangen.
Einführung
Nachdem Oberflächentests auf der RCP-Plattform bisher vor allem Nutzern kommerzieller Tools vorbehalten blieben, haben Entwickler seit letztem Jahr mit dem Eclipse SWTBot-Projekt eine Open Source-Lösung zur Hand. SWTBot ist als “Klick-Roboter” realisiert - in JUnit-Tests wird ausgelöst durch Aufrufe des SWTBot APIs die Oberfläche bedient. Da dabei die echte Anwendung auf dem Bildschirm aktiv ist, können alle Feinheiten der Oberfläche unter realistischen Bedingungen getestet werden. So werden auch Fehlerfälle entdeckt, die nur unter realen Bedingungen auftreten und nachzuvollziehen sind - beispielsweise Probleme bei der Behandlung des Eingabe-Fokus oder mit Nebenläufigkeiten in der Anwendung.
Im Dezember 2008 ist das Projekt von SourceForge zu Eclipse umgezogen. Es befindet sich derzeit in der Incubation-Phase des Eclipse-Entwicklungsprozesses. Das bedeutet, es werden aktuell die Anforderungen umgesetzt, die ein vollwertiges Eclipse-Projekt hinsichtlich seiner Prozesse, Community und Technologie erfüllen muss. Die Code-Basis des Projektes ist bereits stabil und ausgereift. Die Nightly Builds können verwendet werden, um die Technologie zu evaluieren und erste Tests für RCP-Anwendungen zu schreiben. Nichtsdestotrotz müssen Sie noch mit der einen oder anderen Änderung am API rechnen. Aktuell gibt es noch keinen Termin für das Release 2.0 - geplant ist zunächst die Weiterentwicklung und kontinuierliche Veröffentlichung von stabilen Nightly Builds.
Beispielprojekt
Als Beispielprojekt werde ich im Folgenden eine RCP-Tutorialanwendung verwenden. Es handelt sich dabei wie in Abbildung 1 zu erkennen um ein einfaches Adressbuch. Mit Perspektiven, Views, Editoren und Commands kommen darin alle wesentlichen grafischen Bestandteile einer Eclipse RCP-Anwendung vor. Die Anwendung ist daher gut geeignet, die Funktionsweise und das API von SWTBot zu demonstrieren. Wenn Sie das folgende Beispiel nachvollziehen möchten, finden Sie unter “Einrichtung des Beispielprojektes” die Schritte zur Konfiguration des Projektes.

Abbildung 1. Adressbuch-Beispiel
Einrichtung des Beispielprojektes
- Um das Projekt auszuführen, benötigen Sie Eclipse 3.4.2 für Plug-in/RCP Entwickler.
- Laden Sie die Beispielprojekte und entpacken Sie sie lokal.
- Richten Sie in den Eclipse-Einstellungen den Projektordner
target/eclipse/unterPreferences / Plug-in DevelopmentalsTarget Platformein. - Importieren Sie die Projekte mit
File > Import > Existing Projects into Workspacein Ihren lokalen Workspace. - Starten Sie die Anwendung testweise mit
Launch an Eclipse Applicationüber dieplugin.xml-Datei aus demde.rcpbuch.addressbookPlug-in.
SWTBot installieren
SWTBot muss zur Ausführung von Tests in die Eclipse IDE installiert werden. Dies erledigen Sie am einfachsten über die auf der SWTBot Download-Seite bereitgestellten Nightly Builds. Es wird mindestens Eclipse Version 3.4 benötigt, für Version 3.5 stehen separate Builds zur Verfügung. Nach einem Neustart der IDE ist SWTBot einsatzbereit.
Verwenden Sie für Ihre Anwendung eine eigene Target Platform, müssen dieser zur Ausführung von SWTBot-Tests einige Plug-ins hinzugefügt werden. Bei der Target Platform der Beispielanwendung ist dies bereits geschehen, hier wurden die benötigten Plug-ins org.eclipse.swtbot.swt.finder, org.eclipse.swtbot.eclipse.finder, org.eclipse.swtbot.eclipse.core, org.junit4, org.junit, org.hamcrest sowie org.apache.log4j hinzugefügt.
Einen ersten Test erstellen und ausführen
Oberflächentests sollten generell in separaten Plug-ins abgelegt werden. So wird sichergestellt, dass die Abhängigkeiten der Anwendung frei von denen der Tests bleiben. Für einfache Anwendungen genügt meist ein einzelnes Plug-in für alle Tests, in komplexeren Anwendungen sollten ggf. auch die Tests in mehrere Plug-ins aufgeteilt werden, um die Übersichtlichkeit zu bewahren. Abhängigkeiten der Tests zu den Plug-ins der Anwendung können nach Bedarf eingeführt werden, um den Zustand von Modell-Objekten oder Backend-Logik in den Test einzubeziehen.
Bevor es an die Entwicklung der ersten Testmethoden geht, sollte man überprüfen, ob die Ausführung von SWTBot-Tests im Allgemeinen funktioniert. Legen Sie dazu ein neues Plug-in de.rcpbuch.addressbook.tests an. Für neue Tests von Anwendungen ab Java 1.5 empfiehlt sich die Verwendung von JUnit 4, SWTBot unterstützt aber auch noch Version 3.8 des Test-Frameworks. Fügen Sie dem Test-Plug-in folgende Abhängigkeiten hinzu:
- org.junit4
- org.eclipse.swt
- org.eclipse.ui
- org.eclipse.swtbot.swt.finder
- org.eclipse.swtbot.eclipse.finder
- org.hamcrest
Erstellen Sie nun mit File > New > Other > JUnit Test Case einen neuen JUnit 4-Test und fügen Sie zunächst nur eine leere Testmethode hinzu.
SWTBot Tests werden nicht als JUnit Plug-in Test ausgeführt, sondern über eine spezielle SWTBot-Konfiguration gestartet. Diese stellt die Umgebung her, in der die Tests in einem Hintergrund-Thread ausgeführt werden und SWTBot die Anwendung aus diesem heraus fernsteuern kann. Um den ersten Test probeweise auszuführen, wählen Sie per Rechtsklick auf die Testklasse Run as > SWTBot test. In der Startkonfiguration wählen Sie unter Main > Run a product / application die zu startende RCP-Anwendung aus (im Beispiel: de.rcpbuch.addressbook.product) und fügen unter Plug-ins folgende Abhängigkeiten hinzu:
- org.eclipse.swtbot.eclipse.core
- org.eclipse.jdt.junit4.runtime
Dadurch wird die RCP-Anwendung gestartet und nach der Ausführung der Test-Methode automatisch wieder beendet. Sollte dabei etwas schief gehen, untersuchen Sie das Fehlerprotokoll der Anwendung und prüfen die Startkonfiguration auf fehlende Abhängigkeiten.
Funktionsweise und API
Der Haupteintrittspunkt in das SWTBot-Framework ist die Klasse SWTBot. Diese enthält ein umfangreiches API, um SWT-Oberflächen zu bedienen. Für Eclipse RCP-Anwendungen verwendet man die Unterklasse SWTWorkbenchBot, die Erweiterungen zur Bedienung der Eclipse Workbench-Oberfläche bereitstellt. Deklarieren Sie daher in der Testklasse einen SWTWorkbenchBot:
private final SWTWorkbenchBot bot = new SWTWorkbenchBot();
Mit Methodenaufrufen auf dem Bot können Sie nun in den Testmethoden nach Steuerelementen suchen und Aktionen auf diesen auslösen. Dazu verwenden Sie die [widget]With[Condition] Methoden, z.B.:
SWTBotText textField1 = bot.textWithLabel("Name:");
SWTBotText textField2 = bot.textWithTooltip("Name");
Die [widget]WithLabel-Methoden finden Steuerelemente, die auf ein Label mit der angegebenen Beschriftung folgen. So erhält man ein SWTBot[Widget]-Objekt, auf dem man nun Aktionen ausführen kann:
textField1.setFocus();
textField1.typeText("Hello");
SWTBot löst diese Aktionen im GUI aus, indem Ereignisse auf der SWT Ereignis-Warteschlange eingestellt werden. Aus Sicht des SWT-Frameworks sind diese Ereignisse nicht von der Interaktion mit einem realen Anwender zu unterscheiden. Praktisch ist zudem, dass bei den Abfragemethoden immer für eine kurze Zeit auf das jeweilige Widget gewartet wird. Es macht also nichts, wenn ein Widget erst in Folge der vorhergehenden Aktion erscheint. Wird beispielsweise ein Dialog geöffnet, könnte man mit swt.shell("someTitle") auf diesen Dialog warten und dann in dem Dialog weiterarbeiten.
Mit der Möglichkeit, Controls anhand ihrer Beschriftungen zu lokalisieren, können Tests nah an der Vorgehensweise eines realen Anwenders formuliert werden. Nachteil dabei ist jedoch, dass die Tests angepasst werden müssen, wenn sich Beschriftungen ändern. Zudem entsteht bei internationalisierten Anwendungen Mehraufwand bzw. die Tests können nur für eine Sprache ausgeführt werden. Eine Lösung für dieses Problem besteht darin, für die Controls der Anwendung IDs zu vergeben, die SWTBot den Weg weisen:
someControl.setData("org.eclipse.swtbot.widget.key", "someId")
Mit setData werden einem SWT Control Zusatzinformationen angefügt. Der Key hierfür ist mit der Konstante SWTBotPreferences.DEFAULT_KEY definiert. Diese sollten Sie in Ihrer Anwendung jedoch nicht verwenden, um keine Abhängigkeit zu SWTBot einzuführen. Optional deklarieren Sie eine eigene Konstante für diesen Wert. So markierte Controls können Sie mit den [widget]WithId-Methoden lokalisieren. Dies funktioniert besonders robust, da das Control eindeutig markiert und identifiziert wurde. Auch wenn Sie Ihre Maske umbauen, wird keine Änderung am Testcode notwendig:
SWTBotText textField3 = bot.textWithId("someId");
Ebenfalls erfreulich einfach gelöst ist die Bedienung von Menüs. Dieses erreichen Sie über die SWTBot.menu()-Methoden. Analog erfolgt die Steuerung von Kontextmenüs über contextMenu(), beispielsweise:
// Hauptmenü-Eintrag finden und klicken
bot.menu("Datei").menu("Neu").click();
// Tabelleneintrag selektieren und Kontextmenü bedienen
SWTBotTable table = bot.table();
table.select("Heike Winkler");
table.contextMenu("Adresse öffnen").click();
Auf die Workbench zugreifen
Für das Testen von RCP-Anwendungen ist vor allem die zielstrebige Bedienung der Eclipse Workbench entscheidend. SWTBot meistert auch diese Aufgabe mit Bravour. Einige Erweiterungen an SWTBot, die ich mit Bug 271630: SWTBot Improved RCP / Workbench support bereitgestellt habe und die mittlerweile ihren Weg in das offizielle SWTBot-Repository gefunden haben, erleichtern die Navigation in RCP-Oberflächen nochmals.
Mit den Methoden von SWTWorkbenchBot können gezielt die Elemente der Eclipse Workbench angesteuert werden. So können Views oder Editoren anhand ihrer ID oder dem Reiter-Titel aufgefunden werden. Ist noch kein passender Reiter geöffnet, wartet SWTBot auch hier kurze Zeit auf das Erscheinen, nach einem Timeout wird eine WidgetNotFoundException-Exception geworfen, die den Test zum Fehlschlagen bringt. Views und Editoren lokalisieren Sie folgendermaßen:
SWTBotView view1 = bot.viewById("de.rcpbuch.someViewId");
SWTBotView view2 = bot.viewByTitle("Adressen");
SWTBotEditor editor1 = bot.editorById("de.rcpbuch.someEditorId");
SWTBotEditor editor2 = bot.editorByTitle("Hans Schmidt");
Sobald Sie ein solches View- oder Editor-Objekt zur Verfügung haben, können Sie mit der bot()-Methode einen SWTBot erhalten, der auf die Inhalte dieses View bzw. Editor-Reiters beschränkt ist. So können Sie alle Methoden von SWTBot verwenden, um über die Inhalte des Reiters zu navigieren:
SWTBot editorBot = editor1.bot();
editorBot.textWithLabel("Name:").setText("Heike Muster");
Das API von SWTBotView und SWTBotEditor stellt zusätzlich Methoden bereit, um den Reiter selbst zu bedienen. Beispielsweise können Editoren gespeichert und geschlossen werden:
assertTrue(editor1.isDirty());
editor1.save();
assertFalse(editor1.isDirty());
editor1.close(),
Auch vor Perspektiven und Commands macht der SWTBot bei der Fernsteuerung der Eclipse Workbench keinen Halt. Das Prinzip ist dabei analog zu den bereits gezeigten Methoden. In Listing 1 sehen Sie einen ausführlicheren Beispiel-Test für die Adressbuch-Anwendung.

Abbildung 2. Erfolgreicher Testlauf
Listing 1: Vollständiger Beispieltest
public class AddressBookTests {
private final SWTWorkbenchBot bot = new SWTWorkbenchBot();
@Before
public void setup() {
UIThreadRunnable.syncExec(new VoidResult() {
public void run() {
resetWorkbench();
}
});
}
/**
* Ggf. offene Fenster schließen, alle Editoren schliessen, aktuelle
* Perspektive zuruecksetzen, Standard-Perspektive aktivieren, diese auch
* zurücksetzen
*/
private void resetWorkbench() {
try {
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow();
IWorkbenchPage page = workbenchWindow.getActivePage();
Shell activeShell = Display.getCurrent().getActiveShell();
if (activeShell != workbenchWindow.getShell()) {
activeShell.close();
}
page.closeAllEditors(false);
page.resetPerspective();
String defaultPerspectiveId = workbench.getPerspectiveRegistry().getDefaultPerspective();
workbench.showPerspective(defaultPerspectiveId, workbenchWindow);
page.resetPerspective();
} catch (WorkbenchException e) {
throw new RuntimeException(e);
}
}
@Test
public void testAdresseAnlegen() throws Exception {
bot.menu("Datei").menu("Neu").click();
bot.shell("Neu");
SWTBotTree tree = bot.tree();
tree.select("Neue Adresse");
bot.button("Weiter >").click();
bot.text().setText("Otto Muster\nMusterstr. 12\n01234 Musterhausen");
bot.button("Fertig stellen").click();
SWTBotTable table = bot.viewByTitle("Adressen").bot().table();
assertNotNull("Angelegte Adresse nicht in Adressliste", table.getTableItem(
"Otto Muster"));
}
@Test
public void testAdresseEditieren() throws Exception {
SWTBotTable table = bot.viewByTitle("Adressen").bot().table();
table.select("Heike Winkler", "Marion Graf");
table.contextMenu("Adresse öffnen").click();
assertEquals("Zwei Editoren goeffnet", 2, bot.editors().size());
SWTBotEditor editor = bot.activeEditor();
assertEquals("Heike Winkler", editor.getTitle());
assertFalse("Editor ohne aenderung -> nicht dirty", editor.isDirty());
SWTBot editorBot = editor.bot();
SWTBotText text = editorBot.textWithLabel("Name:");
assertTrue("Eingabefokus auf Namensfeld", text.isActive());
text.setText("Heike Muster");
editorBot.textWithLabel("Straße:").setText("Musterstrasse");
editorBot.textWithLabel("PLZ/Ort:").setText("01234 Musterort");
assertTrue("Editor nach Aenderung dirty", editor.isDirty());
new CommandFinder().findCommand(equalTo("Speichern")).get(0).click();
assertFalse("Editor nach dem Speichern nicht dirty", editor.isDirty());
editor.close();
assertNotNull("Aenderung in Adressliste sichtbar", table.getTableItem(
"Heike Muster"));
bot.editorByTitle("Marion Graf").close();
}
@Test
public void testVisitenkarten() throws Exception {
bot.perspectiveByLabel("Visitenkarten").activate();
assertTrue("Visitenkarten-View aktiv", bot.viewByTitle("Visitenkarten").isActive());
}
}
Eigene Matcher mit Hamcrest implementieren
SWTBot verwendet intern zum Auffinden der Steuerelemente das Hamcrest Matcher-Framework. Dabei handelt es sich um eine Bibliothek, die das Abfragen und Kombinieren von Bedingungen über Matcher-Objekte abbildet. Dazu werden die Methoden der Hamcrest-Klassen MatcherAssert und Matchers statisch importiert und aufgerufen, bspw.:
assertThat(someObject, hasProperty("someProperty", containsString("test")));
So können deklarativ Bedingungen beschrieben und nach Bedarf überprüft werden. Neben den mitgelieferten Matchern aus Hamcrest verwendet SWTBot eigene Matcher, um Bedingungen beim Auffinden von Widgets zu evaluieren. Zur Illustration des Prinzips sehen Sie in Listing 2 eine vereinfachte Variante des Matchers, der entscheidet, ob ein Control eine bestimmte ID enthält. Wichtig bei der Implementierung von Matchern für SWTBot ist dabei, dass die Prüfung von GUI-Elementen aus dem UI-Thread heraus erfolgt, denn der Zugriff auf SWT-Objekte ist nur diesem Thread gestattet. SWTBot stellt für diesen Vorgang mit UIThreadRunnable eine Hilfsklasse zur Verfügung.
In SWTBot werden Matcher über statische Factory-Methoden in der Klasse WidgetMatcherFactory instantiiert und beim Lokalisieren von Controls eingesetzt. Möchten Sie in Tests eigene Logik zum Auffinden von Controls verwenden, könnten Sie einen eigenen Matcher implementieren und mit SWTBot.widget(...) zur Abfrage übergeben:
Table table = (Table) bot.widget(
Matchers.allOf(
widgetOfType(Table.class),
withLabel("Test"),
withMyOwnMatcher(123)));
SWTBotTable tableBot = new SWTBotTable(table);
Listing 2: Hamcrest-Matcher WithId
public class WithId<T extends Widget> extends AbstractMatcher<T> {
private final String value;
public WithId(String value) {
this.value = value;
}
protected boolean doMatch(final Object obj) {
String data = UIThreadRunnable.syncExec(new Result<String>() {
public String run() {
return (String) ((Widget) obj).getData(SWTBotPreferences.DEFAULT_KEY);
}
});
return value.equals(data);
}
public void describeTo(Description description) {
description.appendText("with id ".appendText(value);
}
}
Robuste Tests schreiben
Analog zur Entwicklung von regulären Tests ist es auch für die erfolgreiche Umsetzung und Pflege von SWTBot-Testsuiten entscheidend, für jeden Testfall saubere und definierte Ausgangsbedingungen zu gewährleisten. Theoretisch müsste man die gesamte Anwendung vor jeder Testmethode neu starten, um eine unangetastete Umgebung zu verwenden und so die Unabhängigkeit der Tests untereinander zu garantieren. Da dies die Ausführungszeit der Tests sehr in die Höhe treiben würde, müssen Sie hier ein wenig improvisieren.
In jedem Fall sollten die Testmethoden voneinander unabhängig gehalten werden. Kein Testfall sollte auf dem vorherigen aufbauen, da sonst fehlschlagende Tests häufig alle nachfolgenden Tests mitreißen und die Möglichkeit fehlt, einzelne Tests schnell zu verifizieren.
Idealerweise bringen Sie mit einer @Before-Methode die Anwendung vor jedem Testfall in einen ausreichend definierten Zustand. Es empfiehlt sich, alle Editoren zu schließen, die Perspektive zurückzusetzen und wieder zur Startperspektive der Anwendung zurückzukehren. Im Listing 1 sehen Sie auch einen solchen Workbench-Reset.
Ein weiteres Problem sind die Testdaten der Anwendung. Auch hier sollte jede Testmethode einen sauberen Ausgangszustand vorfinden. Sofern hinter der Oberfläche der Anwendung eine Datenbank- oder Service-Schicht steckt, empfiehlt sich die Wiederverwendung von hier ggf. vorhandenen Testdaten. So könnten die Tests der GUI-Anwendung über einen speziellen Aufruf um die Bereitstellung von Testdaten bitten. So können Sie auch alle Werkzeuge verwenden, die für diese Aufgabe bereits existieren (bspw. DBUnit). Eine weitere empfehlenswerte Variante ist das Schreiben von clientseitigen Mocks für die Backend-Schnittstellen. Voraussetzung dafür ist jedoch, dass das Backend bereits gut getestet ist, da die Tests der GUI-Anwendung das Backend-System dann nicht mehr mit abtesten.
Auch wenn das Aufsetzen der Test-Voraussetzungen zu Beginn häufig mühevoll ist, lohnt (und rechnet) sich der Aufwand in den meisten Fällen durch kürzere Laufzeiten der Testsuiten und zuverlässigere, stabilere Testergebnisse.
SWTBot erweitern
Da SWTBot noch ein sehr junges Projekt ist, werden Sie mit Sicherheit hin und wieder an die Grenzen des Machbaren stoßen. Der Einstieg in die Entwicklung von Erweiterungen und Verbesserungen für SWTBot sollte aufgrund der gut strukturierten Code-Basis und ausführlicher Code-Dokumentation jedoch nicht weiter schwerfallen. Eine kurze Anleitung zur Einrichtung der Projekte finden Sie unter Contributing to SWTBot. Ist Ihre Erweiterung auch für andere Entwickler interessant, wäre es eine Überlegung wert, einen Bugreport mit einem Patch im Eclipse-Bugzilla einzustellen. Contributions sind wie bei jedem Open Source-Projekt auch bei SWTBot sehr willkommen.
Fazit
SWTBot ermöglicht es, RCP-Anwendungen mit einfachen JUnit-Tests gründlich zu testen. Das API ist gelungen, das Framework läuft stabil und macht einen robusten Eindruck. Ein Einsatz wird mit schnellen und robusten GUI-Tests belohnt, die Ihnen die Funktionstüchtigkeit und Qualität Ihrer GUI-Implementierung langfristig nachprüfen und absichern.
Links & Literatur
Ralf Ebert
Ralf Ebert ist freiberuflicher Softwareentwickler und Trainer. Er entwickelt seit vielen Jahren Frontends für Java-Systeme und berät seine Kunden bei der Umsetzung von Anwendungen in diesem Bereich. Seine Erfahrungen aus zahlreichen Projekten gibt er auch in seinem Seminaren weiter. Kontakt: info@ralfebert.de.


do you have an english version of this article?
Your words seem to be great, but i could not understand :(