Enterprise Eclipse RCP, Teil 4

Enterprise Eclipse RCP: Continuous Integration

Stefan Reichert

Die in den drei bisherigen Teilen vorgestellten Eclipse-Bordmittel, Methoden und Techniken stellen eine solide Basis zur Entwicklung von verteilten Anwendungen mit Eclipse RCP dar. Die Entwicklung einer verteilten Anwendung erfolgt in der Regel im Team. Nicht nur aus diesem Grund besteht die Notwendigkeit, die gesamte Codebasis automatisiert zu testen. Für gewöhnlich übernimmt diese Aufgabe ein System wie CruiseControl oder Hudson. Der vierte Teil der Serie über Enterprise Eclipse RCP beschäftigt sich mit dem Thema Continous Integration und zeigt, wie Eclipse-RCP-Anwendungen mithilfe von Plug-in-Tests als Ganzes getestet werden können.

Das Ziel eines Continuous-Integration-Prozesses ist vorrangig, die Integrität der gesamten Codebasis einer Anwendung automatisiert zu gewährleisten. Das geschieht vornehmlich über das Kompilieren der Codebasis und die anschließende Durchführung von Unit-Tests, die dann die einzelnen Aspekte des kompilierten Codes prüfen. Dabei ist die Art der durchzuführenden Tests durchaus interessant. Oft reichen „einfache“ Unit-Tests nicht aus, da RCP-Mechanismen wie Extension Points oder Oberflächenteile der Anwendung getestet werden sollen. Die Applikationorg.eclipse.ant.core.antRunner kann in diesem Fall nicht verwendet werden, da z. B. zum Testen von Oberflächenteilen eines Produkts die Application-Klasse des Produkts selbst gestartet werden muss. Die Ausführung dieser so genannten Plug-in-Tests benötigt demzufolge ein exportiertes Produkt, welches verwendet werden kann. Da das Produkt unter realen Bedingungen getestet werden sollte, ist bei einer verteilten Anwendung auch ein laufendes Backend notwendig, damit benötigte Services ansprechbar sind. Der Plug-in-Test erfordert somit auch das automatisierte Bauen und Deployment des Backends.

Abb. 1: Schritte eines Continuous-Integration-Laufs für verteilte Anwendungen mit Plug-in-Tests

Sind sämtliche Plug-in-Tests erfolgreich durchlaufen, werden für gewöhnlich im Anschluss die Artefakte der Anwendung zur Verfügung gestellt – im Fall der in dieser Artikelserie beschriebenen verteilten Anwendung mit Eclipse RCP sind das Produktexport und JEE-Webarchiv.

Der gesamte Prozess, der in Abbildung 1 abgebildet ist, läuft dabei automatisiert auf einem Continuous-Integration-System wie CruiseControl oder Hudson ab, wobei der eigentliche Ablauf typischerweise über ein Ant- oder Maven-Skript gesteuert wird. In diesem Beispiel greife ich auf Ant zurück, um alle Schritte eines Continuous-Integration-Laufs zu steuern. Das betrifft sämtliche Schritte, also auch das Exportieren des Produkts und das Ausführen der Plug-in-Tests.

[ header = Seite 2: Produktexport ]

Produktexport

Der erste Schritt, also das Bauen des JEE-Backends, ist mit Ant-Bordmitteln eher leicht umzusetzen, Gleiches gilt für das Ausführen der JUnit-Tests für die Backend-Logik. Spannender ist da schon der automatisierte Produktexport. Praktischerweise kann hier der PDE-Build [1] verwendet werden, der für gewöhnlich aus dem PDE-Editor im UI gestartet wird. Zwar ist er schon etwas in die Jahre gekommen, aber einmal aufgesetzt arbeitet er beim Bauen von reinen Eclipse-RCP-Anwendungen sehr zuverlässig. Alternativ könnte hier auch Buckminster [2] verwendet werden, wenn ein Provisioning der Target-Platform benötigt wird. Für dieses Beispiel ist der PDE-Build jedoch vollkommen ausreichend und gut integrierbar. Der PDE-Build baut auf Ant-Skripten auf und ist Bestandteil des org.eclipse.pde.build-Bundles. Er kann daher mit der org.eclipse.ant.core.antRunner-Applikation headless gestartet und somit problemlos für einen Continuous-Integration-Lauf genutzt werden. Hierfür sind allerdings ein paar Vorarbeiten notwendig, damit automatisiert gebaut werden kann.

Der PDE-Build fordert eine bestimmte Verzeichnisstruktur, in der er arbeitet. Grundsätzlich setzt sich diese aus folgenden Elementen zusammen:

  • Eine Eclipse-Distribution, die den Build ausführt.
  • Ein Artefaktverzeichnis, das die Plug-ins und Features des Produkts beinhaltet.
  • Die Target-Plattform in Gestalt der Eclipse RCP Runtime Binaries (bzw. einer Eclipse-Distribution) und das Delta Pack, das Teil des zu exportierenden Produkts wird.

Diese Elemente bzw. Ordner befinden sich in einem so genannten Root-Verzeichnis, in dem der PDE-Build-Lauf Zuhause ist. Vor Beginn des Laufs müssen die benötigten Projekte im Artefakt-Verzeichnis zur Verfügung gestellt werden. Die Konfiguration des Laufs erfolgt entweder über eine build.properties-Datei oder mittels JVM-Argument beim Starten des Laufs. Für den Start des Laufs wird der Equinox Launcher und die org.eclipse.ant.core.antRunner-Applikation verwendet, wie in Listing 1 dargestellt ist. Als Ant-Build-Datei dient die productBuild.xml-Datei desorg.eclipse.pde.build-Bundles der Eclipse-Distribution aus dem Root-Verzeichnis. Der Lauf erstellt dann den benötigten Produktexport aus Applications Bundles und Target-Plattform, der im Folgenden benötigt wird.

<java classname="org.eclipse.equinox.launcher.Main" fork="true" failonerror="true" logerror="true">
<arg value="-consoleLog" />
<arg value="-application" />
<arg value="org.eclipse.ant.core.antRunner" />
<arg value="-buildfile" />
<arg value="${eclipseLocation}/plugins/org.eclipse.pde.build_${pdeBuildPluginVersion}/scripts/productBuild/productBuild.xml" />
<arg value="-DrootLocation=${rootLocation}" />
<arg value="-Dtimestamp=${TSTAMP}" />
<arg value="-DbuildType=${build.type}${build.number}" />
<arg value="-DbuildId=foo" />
<arg value="-DforceContextQualifier=${build.type}${build.number}-v${DSTAMP}-${revision.number}" />
<arg value="-DgenerateFeatureVersionSuffix=true" />
<arg value="-Dproduct=com.foo.application/branding/foo.product" />
<classpath>
<pathelement location="${eclipseLocation}/plugins/org.eclipse.equinox.launcher_${equinoxLauncherPluginVersion}.jar" />
</classpath>
</java>
	
Plug-in-Tests

Nachdem das Produkt mittels PDE-Build exportiert wurde, kann es getestet werden. PDE bietet zum Testen von Plug-ins die so genannten JUnit-Plug-in-Tests an [3]. Ein JUnit-Plug-in-Test ist prinzipiell ein ganz normaler JUnit-Test mit dem Unterschied, dass im Hintergrund eine Eclipse RCP läuft, die getestet wird. PDE unterstützt für automatisierte JUnit-Plug-in-Tests allerdings nur die JUnit-3-Notation. Plug-in-Tests leiten somit von der Basisklasse junit.framework.TestCase ab, die Testmethoden beginnen mit dem Präfix test:

/** Tests für das UI. /
public class BaseUITest extends TestCase {
/**
* Testet den Start der Applikation und schaut, ob nach dem Start der
* FooView sichtbar ist.
*/
public void testApplicationStartup() {
IWorkbenchWindow workbenchWindow = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow();
IViewPart viewPart = workbenchWindow.getActivePage()
.findView("com.foo.FooView");
Assert.assertNotNull(viewPart);
}
}

Dieser TestCase prüft, ob nach dem Start der Applikation die View mit der ID com.foo.FooView dargestellt ist. Er greift dafür auf die aktive IWorkbenchPage der laufenden Applikation zu. Zugegeben, der Test ist sehr trivial, allerdings wird deutlich, dass man auf sämtliche Laufzeitelemente einer RCP-Anwendung zugreifen kann, um dessen Zustand zu prüfen. In der IDE kann dieser Test mithilfe von PDE gestartet werden, hierfür ist ein eigener Startkonfigurationstyp verfügbar. Eine Plug-in-Test-Startkonfiguration definiert sowohl den/die auszuführenden Test(s) als auch das für den Test zu startende Produkt bzw. die Applikation.

Abb. 2: Launchkonfiguration der Eclipse IDE zum Starten eines Plug-in-Tests

[ header = Seite 3: Automatisiertes Testen ]

Automatisiertes Testen

Für einen Continuous-Integration-Prozess müssen die JUnit-Plug-in-Tests genau wie der Produktexport automatisiert ablaufen. PDE stellt für den Aufruf per Ant eine passende Applikationorg.eclipse.pde.junit.runtime.uitestapplication zur Verfügung. Listing 2 zeigt die in der Startkonfiguration abgebildete Konfiguration aus Abbildung 2 als Aufruf in Ant.

<java classname="org.eclipse.equinox.launcher.Main" fork="true" failonerror="true">
<arg value="-clean" />
<arg value="-data" />
<arg value="${test.directory}/workspace" />
<arg line="-application org.eclipse.pde.junit.runtime.uitestapplication" />?>
<arg line="-port 2401" />
<arg line="-testApplication com.foo.application" />
<arg line="-testPluginName com.foo" />
<arg line="-className com.foo.BaseUITest" />
<arg value="-Dtimestamp=${timestamp}" />
<classpath>
<pathelement location="${eclipseLocation}/plugins/org.eclipse.equinox.launcher_${equinoxLauncherPluginVersion}.jar" />
</java>

Hier werden alle aus der Startkonfiguration bekannten Einstellungen als Parameter des Java-Prozesses definiert. Lediglich der Parameter –port 2401 verwundert ein wenig. An dieser Stelle wird an die Arbeitsweise des JUnit-Plug-in-Tests erinnert. Die Ergebnisse des Testlaufs werden von der org.eclipse.pde.junit.runtime.uitestapplication-Applikation über einen Listener-Mechanismus bereitgestellt. Dieser Mechanismus bedient eine Socket-Verbindung auf einem zu definierenden Port, in unserem Fall ist das der Port 2401. Um den Test ablaufen zu lassen, muss ein entsprechender Abnehmer auf dem definierten Port gestartet werden. PDE, also die JUnit-Plug-in-Test-Startkonfiguration, nutzt intern einen Listener, der den JUnit-View als Ausgabe verwendet. Die Ergebnisse werden analog zu gewöhnlichen JUnit-Tests dargestellt. Für eine automatisierte Umgebung stellt PDE die PDE Test Utilities [4] zur Verfügung. Die Utilities werden durch das Bundlepde.test.util repräsentiert. Es beinhaltet den PDETestListener, der das Interface ITestRunListener2 implementiert und für die einzelnen Testschritte und Testergebnisse die entsprechenden Methoden definiert:

public void testRunStarted(int testCount);
public void testRunEnded(long elapsedTime);
public void testRunStopped(long elapsedTime);
public void testStarted(String testId, String testName);
public void testEnded(String testId, String testName);
public void testRunTerminated();
public void testFailed(int status, String testId, String testName, String trace, String expected, String actual);
public void testReran(String testId, String testClass, String testName, int status, String trace, String expected, String actual);
	

Wie bereits beschrieben, wird der JUnit-Plug-in-Test mit einer Port-Nummer gestartet, auf der die Ergebnisse zur Verfügung gestellt werden. Der PDETestListener muss entsprechend auf den dort definierten Port horchen. Dafür kann die PDE-Klasse RemoteTestRunnerClient genutzt werden. Mit ihr kann auf einem Port eine Liste von ITestRunListener2 gestartet werden. Alle Listener werden mittels ihrer Methoden über den kompletten Ablauf des Testlaufs informiert und können die gelieferten Informationen in einem entsprechenden Format dokumentieren. Der PDETestListener ist so implementiert, dass er das Testergebnis in einer XML-Datei persistiert, was für das automatisierte Vorgehen ideal ist. Einzig und allein das Starten von RemoteTestRunnerClient ist jetzt noch spannend, da eine bestimmte Reihenfolge einzuhalten ist. Zuerst muss der Listener gestartet werden, bevor der in Listing 2 dargestellte PDE-Plug-in-Test gestartet werden kann. Da der Listener jedoch blockiert – er hört wie beschrieben auf den definierten Port –, muss der PDE-Plug-in-Test in einem zweiten Thread gestartet werden. Ant liefert dazu den parallel-Task, mit dem sich die beiden Prozesse parallel starten lassen:

<target name="run.test">
<parallel>
<daemons>
<antcall target="start.listener" />
</daemons>
<sequential>
<sleep seconds="2" />
<antcall target="execute.test" />
</sequential>
</parallel>
</target>

Das Ausführen des Tests erfolgt mit einer kleinen Verzögerung, um dem Listener die Möglichkeit zum Starten zu geben. Endet der Testlauf, egal mit welchem Ergebnis, dann stoppt der Listener automatisch.

Abb. 3: Ablauf eines JUnit-Plug-in-Tests mit den Hilfsmitteln von PDE

UIs Testen

Nachdem nun deutlich wurde, wie man mit Eclipse-Bordmitteln eine Eclipse-RCP-Anwendung automatisiert testen kann, stellt sich die Frage nach Inhalt und Implementierung der Tests für eine verteilte Anwendung. Zunächst muss darüber nachgedacht werden, was mittels UI-Test geprüft werden soll. Im Vordergrund sollten dabei vor allem das UI und dessen Funktionen stehen. Die Korrektheit der Geschäftslogik des Servers sollte mit JUnit-Tests für die Serverklassen überwacht werden, nicht durch JUnit-Plug-in-Tests auf der Clientseite. Auch sollte darauf geachtet werden, die Testtiefe für die UI-Seite eher flach zu halten. UI-Strukturen sind zumeist sehr komplex, und einzelne Tests, die die volle Funktionalität einer UI-Komponente erfassen, sind meist noch komplexer. Flexibler sind Tests für einzelne Funktionen des UI, wie die Selektion in einer Master-Detail-Ansicht oder das Speichern eines Editors. Diese Tests können leicht angepasst und in der Reihenfolge verändert werden.

Als zweiter wesentlicher Punkt ist dann die tatsächliche Implementierung interessant. Tests lassen sich am besten als Fragmente implementieren. Sie sind somit optionaler Teil des Klassenpfads des zu testenden Bundles, selbiges besitzt aber keinerlei Abhängigkeiten zum Test. Für das Testen des UIs muss auf Inhalte von UI-Elementen zugegriffen werden. Ein Test benötigt somit Zugriff auf den Inhalt von Widgets. Allerdings ist es nicht ratsam, sämtliche Widgets einer Komponente wie die Controls eines Views per Getter- und Setter-Methoden zu veröffentlichen. Alternativ sollten eher „Funktionen“ auf der Komponente veröffentlicht werden. Dafür kann sehr elegant das IAdapter-Interface verwendet werden, das z. B. sämtliche IWorkbenchParts von Eclipse implementieren. Die Methode getAdapter() kann für die Rückgabe einer Art „Fernsteuerung“ verwendet werden, mit der die Oberfläche mittels höherwertiger Funktionen bedienbar ist. Die Fernsteuerung kann im Idealfall auch von der Anwendung selbst benutzt werden, um das UI zu verwenden. Das API der Komponente bleibt so sauber von Einflüssen der Tests und trotzdem flexibel für benötigte Änderungen und Erweiterungen.

Geschrieben von
Stefan Reichert
Stefan Reichert
Stefan Reichert, Diplom-Wirtschaftsinformatiker (FH), arbeitet als Software Engineer bei Zühlke Engineering in Hamburg. Er beschäftigt sich seit mehreren Jahren mit verteilten Java-Enterprise-Anwendungen, serviceorientierten Architekturen, Eclipse und Eclipse RCP. Seit einiger Zeit begeistert ihn die Entwicklung von Apps mit Android. Darüber hinaus ist er Buchautor und verfasst regelmäßig Fachartikel. Kontakt: stefan[at]wickedshell.net, http://www.wickedshell.net/blog
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.