Testen von Eclipse Anwendungen

Inspiriert vonpatrick paulins blog will ich heute mal das Testen von Eclipse Anwendungen unter die Lupe nehmen, und hierbei soll es ausgehend von dessen Erkenntnissen hauptsächlich darum gehen, ein voll funktionsfähiges Beispiel zusammenzubauen, das es uns ermöglicht, die komplette Anwendung headless zu testen.

Hier legen wir zunächst fest:

  • Tests für einzelne Plug-Ins sind in den jeweiligen Fragmenten zu den Plug-Ins abgelegt. Für die Vorteile die sich daraus ergeben, kann man direkt bei Patrick´s Blog vorbeischauen
  • Wir verwenden hierfür Patricks BundleTestCollector, der alle Tests innerhalb eines Workspaces in einer TestSuite zusammenfasst, Problem ist einfach gesagt – Der JUnit-Testrunner von Eclipse kann zwar problemlos alle Testfälle in einem Plug-In ausführen, allerdings gibt es bisher keine Möglichkeit, alle Testfälle innerhalb eines Workspaces ausführen, so das man, wenn man für alle Plug-Ins Tests in Fragmenten entwickelt hat, jedes Fragment einzeln als Junit-Plug-In Test ausführen müsste.
  • Um den BundleTestCollector auszuführen müssen nur die in diesem Eintrag von Patrick beschriebenen Schritte beachtet werden.

Was wir hier ausserdem verwenden ist das Eclipse Test Framework, hier aktuell in der Version 3.4.
Was das Testframework im Prinzip bereitstellt ist ein Set von Plug-Ins, die es erlauben, den JUnit-Testrunner auch im Headless Mode ablaufen zu lassen. Wie genau das funktioniert soll im folgenden Artikel beschrieben werden.

Hierfür downloaden wir:

Diese stellen wir in einem lokalen Verzeichnis wie d:eclipse_testing bereit.

verzeichnis

Um das Testen zu „testen“ erstellen wir uns eine Demoapplikation. Hier importieren wir zunächst den BundleTestCollector in den aktuellen Workspace.

btc

Der BundleTestCollector liegt das Plug-In „com.rcpquickstart.bundletestcollector.testsuiteexample“ bei, das im Prinzip schon eine fertige Konfiguration bereitstellt.

Hierin befindet sich die Klasse „AllTests“, die eine Suite bereitstellt. Für die Konfiguration machen wir folgendes:

TestSuite suite = new TestSuite(„All Tests“);
testCollector.collectTests(suite, „de.md.“, „de.md.“,“*Test“);

Jetzt sucht unser BundleTestCollector in allen Bundles die mit dem String „de.md.“ anfangen (2. Parameter), nach Packages die mit „de.md.“ anfangen (3. Parameter) nach Tests die dem Pattern „*Test“ ensprechen, also
alle Klassen der Form <…>Test.java.

Wichtig, für die ersten Schritte funktioniert das ganze nur für JUnit3, wie das ganze mit JUnit 4 zum Laufen gebracht werden kann wird in einem weiteren Blogeintrag beschrieben.

Nun erstellen wir eine einfache Demoanwendung.

Hierzu erstellen wir ein neues Plug-In Projekt (und achten natürlich darauf, das dies unseren Patterns entspricht, die wir für den BundleTestCollector bereitgestellt haben.

testing

und nutzen hierfür das Plug-In Template

withaview

Zum Testen führen wir das ganze gleich mal als „Eclipse Application“ aus, und sehen dann das uns allen bekannte Applikationsfenster:

demo

Ohne Pause machen wir gleich weiter und erstellen hierfür einen Test. Leider gibt die einfache Anwendung nicht wirklich viel her, was es zu testen geben kann. Also bauen wir uns schnell mal was zusammen, und zwar soll in dem Viewer statt einfachen Strings einfach mal Objekte vom Typ „Person“ angezeigt werden, hierfür definieren wir uns zunächst den einfachen Typ als POJO:

public class Person {

private String name;
private String email;
private String kdnr;

public Person(String name, String email, String kdnr){
this.name = name;
this.email = email;
this.kdnr = kdnr;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getKdnr() {
return kdnr;
}
public void setKdnr(String kdnr) {
this.kdnr = kdnr;
}
}

und einen „DAO“ für Personen-Instanzen:

public interface IPersonDAOService {

public List<Person> getPersons();

/**
* Liefert die erste Person mit entsprechendem Namen
* */
public Person getPersonByName(String name);

/***/
public void addPerson(Person person);
/***/
public void removePerson(Person person);
}

Dessen Implementierung ist trivial, und braucht hier nicht besprochen werden. Um das ganze schön verfügbar zu machen, verwenden wir den Workbench-Servicelocator-Mechanismus.

Hierfür brauchen wir eine ServiceLocator-Factory, und erstellen hierfür eine Extension für den Extension-Point „org.eclipse.ui.services“

service

Dieser Extension-Point erwartet eine Implementierung von „org.eclipse.ui.services.AbstractServiceFactory“.

servicefactory1

Die Implementierung erwartet folgende Methode, die wir einfach ganz simpel implementieren:

@Override
public Object create(Class serviceInterface, IServiceLocator parentLocator,
IServiceLocator locator) {
return new PersonDAOService();
}

Die komplette Deklaration sieht dann so aus:

<extension
point=“org.eclipse.ui.services“>
<serviceFactory
factoryClass=“de.md.rcp.domain.PersonServiceFactory“>
<service
serviceClass=“de.md.rcp.domain.IPersonDAOService“></service>
</serviceFactory>
</extension>

Damit der ServiceLocator was zum auffinden hat, initialisieren wir im Konstruktor noch 4 Personen-Objekte.

public PersonDAOService(){
addPerson(new Person(„Mueller“,“mueller@tests.de“,“1″));
addPerson(new Person(„Meier“,“meier@tests.de“,“2″));
addPerson(new Person(„Häfele“,“haefele@tests.de“,“3″));
addPerson(new Person(„Geiger“,“geiger@tests.de“,“4″));

}

Diesen Service bauen wir jetzt in den View ein, indem wir zum einen eine Instanz von IPersonDAOService als Instanzvariable deklarieren, und diese in der Methode „createPartControl“ befüllen.

personService = (IPersonDAOService)getSite().getService(IPersonDAOService.class);
Die Personen sollen jetzt natürlich im entsprechenden Viewer angezeigt werden, hierzu passen wir den Viewer folgendermassen an:

class ViewContentProvider implements IStructuredContentProvider {

public void inputChanged(Viewer v, Object oldInput, Object newInput) {
}

public void dispose() {
}

public Object[] getElements(Object parent) {
return personService.getPersons().toArray();
}
}

Zur Anzeige muss natürlich der Labelprovider ebenfalls angepasst werden.

class ViewLabelProvider extends LabelProvider implements
ITableLabelProvider {
public String getColumnText(Object obj, int index) {
Person p = (Person) obj;
switch (index) {
case 0:
return p.getKdnr();
case 1:
return p.getName();
case 2:
return p.getEmail();
default:
throw new IllegalArgumentException(„Illegal Index“);
}

}

public Image getColumnImage(Object obj, int index) {
return null;
}

public Image getImage(Object obj) {
return PlatformUI.getWorkbench().getSharedImages().getImage(
ISharedImages.IMG_OBJ_ELEMENT);
}
}

Die CreatePartControl-Methode des Views sieht dann so aus, hier initialisieren wir zunächst den Service und bauen dann die entsprechende Tabelle zusammen. Das sieht so aus:

personService = (IPersonDAOService) getSite().getService(
IPersonDAOService.class);
viewer = new TableViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);

Table table = viewer.getTable();
table.setHeaderVisible(true);
table.setLinesVisible(true);

// viewer.getTable().getColumn(0).setText(„Kdnr“);
for (int i = 0; i < 3; i++) {
TableColumn c = new TableColumn(table, SWT.NONE);
c.setWidth(130);
switch(i){
case 0 : c.setText(„KDNR“);break;
case 1: c.setText(„name“);break;
case 2: c.setText(„email“);break;
}
}

viewer.setContentProvider(new ViewContentProvider());
viewer.setLabelProvider(new ViewLabelProvider());
viewer.setInput(personService.getPersons());

tabelle1

Ok, viel drumherum, jetzt fangen wir mit dem eigentlichen Thema an, und zwar entwickeln wir ein Testfragment für das Plug-In. Warum ein Fragment? Weil wir ausserhalb des Plug-Ins, aufgrund der Kapselung, keinen Zugriff auf die Implementierung des Personen-Services haben (Ok, eigentlich hätten wir schon Zugriff über den ServiceLocator-Mechanismus, das gilt zum Einen aber nur in diesem Spezialfall und lange nicht für alle und zum anderen muss für die Verwendung des ServiceLocator-Mechanismus eine Workbench-Instanz gestartet sein. Beides können wir nicht voraussetzen, also Tests ab in ein Fragment, denn ein Fragment erweitert im Prinzip den Scope eines Plug-Ins, so dass zur Laufzeit die Tests quasi direkt in das ursprüngliche Plug-In integriert sind, und infolgedessen direkten Zugriff auf die internen Klassen haben.

fragment

Hierbei ist wieder darauf zu achten, daß die Namensgebung dem Pattern für den Bundletestcollector entspricht, als Host wählen wir das zu testende Plug-In aus. Für das Plug-In deklarieren wir eine Dependency für JUnit3 ( da Junit 4 momentan noch nicht funktioniert)

depend

fragment2

Ein für mich guter Pattern ist, das die Tests für bestimmte Klassen im selben Package liegen, wie die zu testenden Klassen.  Da wir die Klasse „PersonDAOService“ testen wollen, legen wir den Test hierfür in das Package „de.md.rcp.domain“ im Fragment.

Der Test den wir definieren testet zunächst die Funktionalitäten „add“ , „remove“ und „getAll“ und könnte so aussehen:

public class PersonDAOServiceTest extends TestCase{

private PersonDAOService service;

public void setUp(){
service = new PersonDAOService();
}

public void tearDown(){
service = null;
}

public void testAdd(){
Person p = new Person(„123″,“Dilger“,“md@test.de“);
assertFalse(service.getPersons().contains(p));
service.addPerson(p);
assertTrue(service.getPersons().contains(p));
}

public void testRemove(){
Person p = new Person(„123″,“Dilger“,“md@test.de“);
assertFalse(service.getPersons().contains(p));
service.addPerson(p);
assertTrue(service.getPersons().contains(p));
service.removePerson(p);
assertFalse(service.getPersons().contains(p));
}

public void testAll(){
// da mit 4 Personen initialisiert
assertEquals(service.getPersons().size(),4);
service.addPerson(new Person(„123″,“Maier“,“mair@test.de“));
assertEquals(service.getPersons().size(),5);

}

}

Lassen wir das ganze mal als JUnit-Test laufen

junit

sehen wir sofort das beruhigende Grün, das mich ruhig schlafen lässt.

junit2

Ok, damit haben wir zunächst mal alles was wir brauchen. Jetzt geht der eigentlich Inhalt dieses Artikels los:-)

Was also brauchen wir, um das Ganze auch Headless auszuführen?

Zunächst mal müssen wir für unser Projekt ein Feature und eine Product-Konfiguration erstellen, das ist grundvoraussetzung, damit das Ganze funktionieren kann. Also definieren wir zunächst mal ein Feature.

feature1

Innerhalb dieses Features definieren wir folgende Abhängigkeiten:

feature_dep

Dieses Feature verwenden wir für den einfachen Product-Build, hierfür definieren wir zunächst mal eine product-definition.

product

Innerhalb des Products definieren wir folgendes:

product_feature

depp

Zu rÜberprüfung, ob die Konfiguration korrekt ist, kann einfach mal das Product direktals Eclipse Application gestartet werden, treten hier Fehler auf, muss die Konfiguration nochmals überprüft werden.

Für die automatisierten Tests definieren wir ein weiteres Feature.

testfeature

und fügen es in die product-konfiguration ein

feature_deps

Im aktuellen Zustand kann die APplikation direkt unter Export exportiert werden. Was wir aber eigentlich möchten, ist genau das automatisiert über die Kommandozeile anzustossen, und gleichzeitig alle Tests ausführen zu lassen. Dies könnte beispielsweise verwendet werden, um den Build an einen CruiseControl zu delegieren.

Wenn wir bis jetzt alles richtig gemacht haben, dann können wir den BundleTestCollector als Junit-Plugin-Test ausführen und unser deklarierter Test sollte automatisch geladen und ausgeführt werden.

bundletest1

Nun definieren wir ein weiteres Projekt, welches einfach als Container für unsere Konfigurationen dient.

Hier definieren wir zum Einen eine Datei build.xml, die uns als Ant-Konfiguration dient, und kopieren die Properties-Datei, die Eclipse für einen Headlessbuild benötigt. Diese Datei befindet sich im Plugin-Verzeichnis eurer Eclipse Installation im folgenden Ordner:

<eclipse_home>pluginsorg.eclipse.pde.build_<version>templatesheadless-buildbuild.properties

Der vorerst komplizierteste Part ist das Erzeugen der entsprechenden Ant-Datei. Dies muss im Prinzip folgende Tasks ausführen.

  • Kopieren der benötigten Plug-Ins und Featuers in ein Target-Directory
  • Bereitstellen der Eclipse-SDK und Testing-Frameworks in das Root des Target-Directories

Patrick Paulin bietet hierfür ein fertiges Skript, das ich jedoch hier nicht verwenden möchte, sondern wir bauen hier zum AUfbau eines tiefergehenden Verständnisses unser eigenen Skript. Das Skript von Patrick gibts hier, das hier entwickelte Skript lehnt sich an dieses an (danke, Patrick;)

Zunächst passen wir jedoch die Datei „build.properties“ an, und zwar folgende:

  • product=/plugin or feature id/path/to/.product nach product=/de.md.rcp.demo/rcpproduct.product
  • configs=*,*,* nach configs = win32,win32,x86
  • buildDirectory=${user.home}/eclipse.build  nach buildDirectory=D:/eclipse_testing/build (hier werden alle Dateien hinkopiert und der Build findet hier statt)
  • base=<path/to/parent/of/eclipse> nach base=<auf das Verzeichnis eurer TargetPlattform oder einer aktuellen Eclipse Installatin>
  • baseLocation wird direkt auf $base gesetzt.
  • javacSource=1.6 (ich möchte den @OVerride Tag weiterbenutzen)
  • javacTarget=1.6 (ich möchte den @Override Tag weiterbenutzen)
  • buildId=MeinBuild (wird später gebraucht und gibt den Teil des Dateinamens für das Zip-File an)
  • archivePrefix=MeinBuild

Folgende Attribute fügen wir neu ein (Convenience nach Patrick Paulin), die Werte dieser Attribute müssten aus eurer aktuellen Eclipse installation ausgelesen werden, die Versionsnummern befinden sich in den Dateinamen der entsprechenden Plugins. Dies sind die aktuellen Versionsnummern für die Eclipse Version 3.4.0

  • pdeBuildPluginVersion=3.4.0.v20080604
  • equinoxLauncherPluginVersion=1.0.100.v20080509-1800
  • testDirectory=D:/eclipse_testing
  • eclipseLocation=<auf das Verzeichnis eurer TargetPlattform, bzw einer aktuellen Eclipse-Installation>

Hierfür definieren wir zunächst ein Target, welches unsere zuvor angepasst Property-File lädt:

<target name=“init“>
<property file=“build.properties“ />
</target>

damit stehen uns schonmal alle Properties zur Verfügung.

Als nächstes erstellen wir ein Target, welches uns die entsprechenden Verzeichnisse erstellt, und die benötigten Plug-Ins dahin kopiert.

<target name=“createAndCopy“ depends=“init“>
<mkdir dir=“${buildDirectory}“ />
<mkdir dir=“${buildDirectory}/plugins“ />
<mkdir dir=“${buildDirectory}/features“ />
<copy todir=“${buildDirectory}/plugins“>
<fileset dir=“../“>
<include name=“de.md.*/**“ />
<include name=“com.rcp.*/**“/>
<exclude name=“de.md.*feature/**“ />
</fileset>
</copy>
<copy todir=“${buildDirectory}/features“>
<fileset dir=“../“>
<include name=“de.md.*feature/**“ />
</fileset>
</copy>

</target>

Wichtig hierbei ist die Zeile <include name=“com.rcp.*/**“/>, da wir natürlich für den Headless-Build auch den BundleTestCollector brauchen.

Führen wir das File aus, sollte die entsprechende Verzeichnisstruktur erstellt werden.

Um einen Build anzustossen, brauchen wir ein weiteres Target. Das sieht folgendermassen aus:

<target name=“pde-build“ depends=“init“>
<java classname=“org.eclipse.equinox.launcher.Main“ fork=“true“ failonerror=“true“>
<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=“-Dtimestamp=${timestamp}“ />
<classpath>
<pathelement location=“${eclipseLocation}/plugins/org.eclipse.equinox.launcher_${equinoxLauncherPluginVersion}.jar“ />
<!– replace with following for Eclipse 3.2 –>
<!– <pathelement location=“${eclipseLocation}/startup.jar“ />–>
</classpath>
</java>
</target>

Führt man dsa Buildfile jetzt aus, sollte folgendes erscheinen:

BUILD SUCCESSFUL
Total time: 11 seconds

Damit wäre der erste Schritt schonmal erfolgreich. Nun wollen wir, das beim Build direkt auch die Tests durchgeführt werden.

Hierfür müssen wir einige weitere Tasks definieren

Wichtig ist, nach Patrick Paulin, dsa für das Testen immer ein frisches Eclipse SDK bereitgestellt wird. Deswegen habe ich am Anfang den Download des aktuellen Eclipse SDK vorgeschlagen. Wird kein frisches SDK verwendet, muss man sich mit allerlei Meta-Files rumärgern, was den Build enorm verlangsamen kann.

Der erste Schritt zum einleiten des Builds ist also, das SDK zu entzippen.

<unzip src=“${testDirectory}/eclipse-SDK-3.4-win32.zip“ dest=“${buildDirectory}“/>

Anschliessend entzippen wir das Testframework, das ja auch als zip-File vorliegt.

<unzip src=“${testDirectory}/eclipse-test-framework-3.4.zip“ dest=“${testDirectory}“/>

anschliessend wird das zuvor gebaute product in das testverzeichnis kopiert.

<unzip src=“${buildDirectory}/${buildLabel}/${buildId}-win32.win32.x86.zip“ dest=“${testDirectory}“/>

Das komplette Target sieht dann so aus:

<target name=“test“>

<unzip src=“${testDirectory}/eclipse-SDK-3.4-win32.zip“ dest=“${testDirectory}“/>
<unzip src=“${testDirectory}/eclipse-test-framework-3.4.zip“ dest=“${testDirectory}“/>
<unzip src=“${buildDirectory}/${buildLabel}/${buildId}-win32.win32.x86.zip“ dest=“${testDirectory}“/>
<move todir=“${buildDirectory}“>
<fileset dir=“${testDirectory}/${archivePrefix}“>
<include name=“*/**“ />
</fileset>
</move>
<property name=“library-file“ value=“${buildDirectory}/plugins/org.eclipse.test_3.2.0/library.xml“/>
<ant target=“core-test“ antfile=“${library-file}“ dir=“${testDirectory}“>
<property name=“os“ value=“${baseos}“/>
<property name=“ws“ value=“${basews}“/>
<property name=“arch“ value=“${basearch}“/>
<property name=“data-dir“ value=“${base}/junit-workspace -clean“ />
<property name=“plugin-name“ value=“de.md.rcp.suite“ />
<property name=“classname“ value=“de.md.rcp.suite.AllTests“ />
<property name=“extraVMargs“ value=“-Demma.coverage.out.file=${coverageDirectory}/coverage.emma -Dosgi.parentClassloader=ext“ />
</ant>

<ant target=“collect“ antfile=“${library-file}“ dir=“${buildDirectory}“>
<property name=“includes“ value=“com*.xml“ />
<property name=“output-file“ value=“test-results.xml“ />
</ant>
<move file=“${buildDirectory}/test-results.xml“ todir=“${testDirectory}“ />
</target>

Advertisements

Ein Gedanke zu „Testen von Eclipse Anwendungen

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s