Testen von RCP-Anwendungen mit SWTBot
SWTBot ist ein Open-Source-Projekt, welches sich zum Ziel gesetzt hat, ein Tool zum Testen von SWT-Anwendungen bereitzustellen. 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.
Aktuell befindet es sich 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 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.
Einen Test erstellen und ausführen
Zur Ausführung von SWTBot-Tests müssen der Target Platform lediglich einige Plug-ins hinzugefügt werden. Diese finden Sie auf der SWTBot-Downloadseite bzw. auf der CD unter /software/swtbot. In einigen Anleitungen wird noch die Notwendigkeit eines IDE-Plugins erwähnt - dies ist mit Eclipse 3.5 obsolet und wird nicht mehr benötigt.
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 com.example.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.eclipse.uiorg.junit4,org.hamcrestorg.eclipse.swtbot.swt.finder,org.eclipse.swtbot.eclipse.finder
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 als JUnit Plug-in Test ausgeführt. Um den ersten Test probeweise auszuführen, wählen Sie per Rechtsklick auf die Testklasse Run as > JUnit Plug-in test. In der Startkonfiguration wählen Sie unter Test die Option Run in UI thread ab (SWTBot-Tests dürfen nicht auf dem UI-Thread ausgeführt werden). Unter Main > Run a product / application wählen Sie die zu startende RCP-Anwendung.
Dadurch wird die RCP-Anwendung gestartet und nach der Ausführung der Test-Methode wieder beendet. Sollte dabei etwas schief gehen, untersuchen Sie das Fehlerprotokoll der Anwendung und prüfen insbesondere 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 aus, indem es Ereignisse auf der SWT Ereignis-Warteschlange einstellt. Aus Sicht der Anwendung 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 bot.shell("title") auf diesen Dialog warten und dann in dem Dialog weiterarbeiten.
Steuerelemente referenzieren
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. Wurde die Applikation mit dem Eclipse-NLS-Mechanismus übersetzt, können Sie dieses Problem lösen, indem Sie die Messages-Konstantenklassen exportieren und in den Tests verwenden. Eine alternative Möglichkeit 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");
Menüs
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. 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 geworfen, die den Test zum Fehlschlagen bringt.
Views und Editoren lokalisieren Sie folgendermaßen:
SWTBotView view1 = bot.viewById("com.example.someViewId");
SWTBotView view2 = bot.viewByTitle("Adressen");
SWTBotEditor editor1 = bot.editorById("com.example.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 alle Methoden von SWTBot verwendet werden, um in den Inhalten 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() {
// Zugriffe auf das UI / SWT-Komponenten muss im
// UI-Thread erfolgen
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();
}
}
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, muss hier ein wenig improvisiert werden.
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 in resetWorkbench auch ein Zurücksetzen der Workbench.
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 Aufruf um die Bereitstellung von Testdaten bitten. So können auch alle existierenden Werkzeuge verwendet werden, z.B. das DBUnit-Framework. Eine weitere empfehlenswerte Variante ist das Schreiben von client-seitigen Mocks für die Backend-Schnittstellen. Voraussetzung ist jedoch, dass das Backend selbst bereits gut getestet ist, da die Tests der GUI-Anwendung das Backend-System dann nicht mehr mit testen.
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 Bug-Report mit einem Patch im Eclipse-Bugzilla einzustellen. Contributions sind wie bei jedem Open Source-Projekt auch bei SWTBot sehr willkommen.


