RCP UI Testing mal ganz ohne Framework

Das Testen von UI Funktionalitäten ist oftmals eine nicht ganz einfache Aufgabe.

Ein Framework, dass die Arbeit hier sehr erleichtert ist beispielsweise SWTBot.

Manchmal lässt die Projektsituation aber einfach nicht zu, ein weiteres Framework in die Systemarchitektur zu integrieren.

Jetzt hat man prinzipiell zwei Möglichkeiten

  1. Wir verzichten auf UI-Tests
  2. Man testet ohne Framework

Dass die erste Option nicht unbedingt zu empfehlen ist, sollte jedem Entwickler klar sein, ebenso aber auch dass die Umsetzung der zweiten Option nicht ganz trivial ist. Hier führen mit Sicherheit mehrere Wege nach Rom, einen davon (nicht zwangsläufig den besten oder kürzesten) soll dieser Artikel beleuchten.

Bevor sich ein Wandersmann auf den Weg macht, sollte die Ausrüstung überprüft werden. Statt gutem Schuhwerk und wasserfester Kleidung benötigen wir jedoch folgendes:

  • Databinding Framework
  • Eclipse Adapter Framework
  • Motivation

Machen wir uns also auf den Weg.

Die ersten Schritte sind meist die schwersten (bzw. die am wenigsten spannenden). Für einen Artikel ist dies meist die Umsetzung einer mehr oder minder spannenden Demoapplikation. Für unsere Zwecke brauchen wir also eine Anwendung mit Benutzeroberfläche, die es zu testen gilt.

Was sind aber testbare UI-Funktionalitäten?

  • Validierungslogik
  • Konvertierungslogik
  • Enablement / Disablement von Widgets
  • Visibility / Invisibility von Widgets

Diese Liste könnte wahrscheinlich endlos weitergeführt werden, aber das soll erstmal reichen.

Wir verwenden hierfür eine sehr einfache Demoanwendung. Das zugrundeliegende Modell besteht nur aus einer Klasse „Person“,

die in etwa so aussieht:

public class Person {

private String name;
private String surname;
private String email;
private String title;

private PropertyChangeSupport support = new PropertyChangeSupport(this);

/**
* Constructor
* */
public Person(String name, String email, String title) {
super();
this.name = name;
this.email = email;
this.title = title;
}

public void addPropertyChangeListener(String attribute,
PropertyChangeListener listener) {
support.addPropertyChangeListener(attribute, listener);
}

public void removePropertyChangeListener(String attribute,
PropertyChangeListener listener) {
support.removePropertyChangeListener(attribute, listener);
}

public String getName() {
return name;
}

public void setName(String name) {
String oldName = this.name;
this.name = name;
support.firePropertyChange(„name“, oldName, name);
}

public String getSurname() {
return surname;
}

public void setSurname(String surname) {
String oldSurname = this.surname;
this.surname = surname;
support.firePropertyChange(„surname“, oldSurname, surname);
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
String oldEmail = this.email;
this.email = email;
support.firePropertyChange(„email“, oldEmail, email);
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
String oldTitle = this.title;
this.title = title;
support.firePropertyChange(„title“, oldTitle, title);
}

}

Damit das Modellobjekt korrekt mit dem Databinding-Framework zusammenarbeitet, implementieren wir den PropertyChangeSupport derart, dass jede Setter-Methode ein PropertyChangeEvent feuert. Hierdurch ist sichergestellt, dass wann immer sich ein Modellattribut ändert, die Änderung an die UI propagiert wird (Ist diese Anforderung nicht zwingend kann auf die Implementierung des PropertyChangeSupport verzichtet werden).

Im Folgenden wird auf die Erläuterung der Standardfunktionalitäten verzichtet, diese sollte man entweder kennen oder nachschlagen.Erstellen wir eine passende Benutzeroberfläche hierfür. Wir erzeugen ein einfaches Demoprojekt als RCP Anwendung.

Zunächst erzeugen wir einen Editor mit einem passenden EditorInput.

Der Code des EditorInputs könnte in etwa so aussehen:

public class PersonEditorInput implements IEditorInput {

private Person model;

public PersonEditorInput(Person person) {
this.model = person;
}

@Override
public boolean exists() {
// TODO Auto-generated method stub
return false;
}

@Override
public ImageDescriptor getImageDescriptor() {
// TODO Auto-generated method stub
return null;
}

@Override
public String getName() {
return „PersonEditor“;
}

@Override
public IPersistableElement getPersistable() {
// TODO Auto-generated method stub
return null;
}

@Override
public String getToolTipText() {
return getName();
}

@Override
public Object getAdapter(Class adapter) {
return Platform.getAdapterManager().getAdapter(model, adapter);
}

public Person getInput() {
return model;
}

}

Die einzig interessante Codezeile ist folgende:

@Override
public Object getAdapter(Class adapter) {
return Platform.getAdapterManager().getAdapter(model, adapter);
}

Darauf kommen wir später noch zu sprechen.

Startet man die Anwendung, könnte das in etwa so aussehen:

pluginwithaview

Hier erstellen wir uns jetzt eine passende Benutzeroberfläche (auf die Erstellung gehe ich nicht näher ein).

view_mit_widgets

Das sieht doch schon gar nicht mal schlecht aus. Zunächst definieren wir Logik, die in diesem Zusammenhang getestet werden könnte (ohne bestimmte Reihenfolge).

  • Der Save-Button sollte nur enabled sein, wenn das Emailfeld korrekt befüllt ist oder die Checkbox deselektiert
  • Das Emailfeld sollte nur enabled sein, wenn die Checkbox selektiert ist
  • Wir die Checkbox abgewählt, sollte eine evtl. zuvor eingegebene Emailadresse gelöscht werden

Derartige Funktionalitäten zu testen ist schwer, da wir nicht einfach eine View für einen JUnit-Test instantiieren können, und selbst wenn das möglich wäre, müssten die einzelnen Felder (jetzt als private deklarierten Felder) irgendwie zugänglich gemacht werden, damit wir sie in einem JUnit-Test abdecken können (sieht man mal von der wenig schönen Möglichkeit ab, das Ganze über die Reflection-API zu lösen).

Gehen wir´s an!

Es gibt einen wunderschönen Design-Pattern, der uns die Testbarkeit zurückgibt, die uns im Verlauf des Weges abhanden gekommen ist, das Presentation Model, ursprünglich definiert von Martin Fowler, hunderte Male adaptiert und jetzt schliesslich hier gelandet. Auf die Philosophie des Design-Patterns, als auch auf mögliche Vor- und Nachteile werde ich nicht näher eingehen und verweise auf entsprechende Fachliteratur oder Online-Artikel. Uns interessiert an dieser Stelle primär die Implementierung.

Ein Presentation-Modell kann als eine Art Abstraktion für unsere View gesehen werden. Das klingt jetzt vielleicht schwierig, ist aber eigentlich sehr einfach.

Gibt es beispielsweise in der View eine Checkbox, dann bietet das PresentationModell hierfür Methoden wie

  • isCheckBoxSelected()
  • isCheckBoxEnabled()
  • isCheckBoxVisible()

Für ein Textfeld könnte das PresentationModell u.a. folgende Methoden bieten:

  • isTextFieldVisible()
  • isTextFieldEnabled()
  • isTextFieldEditable

Im Prinzip machen wir also bestimmte Eigenschaften unsere (natürlich privaten) Widgets nach aussen hin zugänglich, aber nur indirekt gekapselt durch das PresentationModel.Würde man sich das Ganze in einem Klassendiagramm visualisieren, würde das so aussehen:

pm_klassendiagramm

D.h. der View kennt das PresentationModell, und das PresentationModell kennt das Modellobjekt, das wars.

Da wir aber RCP verwenden, wollen wir das ganze noch ein wenig „loser“ koppeln.

Jetzt definieren wir uns das PresentationModel, gemäss den obigen Testanforderungen könnte eine Implementierung wie folgt aussehen, der Einfachheit halber beschränken wir uns auf das Testen des Emailfeldes und der damit zusammenhängenden Funktionalitäten, mögliche Validierungen für die anderen Felder überlasse ich dem Leser.

Ein Interface für das PresentationModel könnte so aussehen:

public interface IPersonPresentationModel {

public IObservableValue isEmailFieldVAlid();

public IObservableValue isEmailButtonChecked();

public IObservableValue isEmailFieldEnabled();

public IObservableValue isSaveButtonEnabled();

public IObservableValue getEmailTextValue();

public IObservableValue getNameValue();

public IObservableValue getTitleValue();

}

Wie man sieht, gibt es eine Methode die den Status der Checkbox überwacht, eine Methode für das Enablement des Emailfeldes (dies hängt von der Selektion der Checkbox ab), sowie eine Methode die das Enablement des Save-Buttons überwacht.Zusätzlich gibt es Getter für alle Observables zur Überwachung der Inhalte, also beispielsweise getNameValue() zur Überwachung des Textes in einem Textfeld.

Da der Editor, den wir hier überwachen, von der Eclipse-Plattform zur Laufzeit instantiiert wird, brauchen wir eine einfache Möglichkeit, das PresentationModel dem Editor bekannt zu machen.

Wir verwenden das Adapter-Framework von Eclipse. Adaptiert wird das Model-Objekt direkt, und zwar aus dem EditorInput. Hier kommen wir wieder zurück auf die zuvor definierte Zeile.

@Override
public Object getAdapter(Class adapter) {
return Platform.getAdapterManager().getAdapter(model, adapter);
}

Wir definieren hierzu eine Extension für den Extension-Point „org.eclipse.core.runtime.adapters“ wie folgt:

<extension
point=“org.eclipse.core.runtime.adapters“>
<factory
adaptableType=“de.pentasys.uitesting.demo.model.Person“
class=“de.pentasys.uitesting.demo.adapters.PersonViewAdapterFactory“>
<adapter
type=“de.pentasys.uitesting.demo.pm.IPersonPresentationModel“>
</adapter>
</factory>
</extension>

Die entsprechende Implementierung der Adapterfactory ist sehr einfach und sieht so aus:

public class PersonViewAdapterFactory implements IAdapterFactory {

public static final Class<?>[] ADAPTERS = new Class<?>[] { Person.class};

@Override
@SuppressWarnings(„unchecked“)
public Object getAdapter(Object adaptableObject, Class adapterType) {

return new PersonViewPresentationModel((Person)adaptableObject);

}

@Override
public Class<?>[] getAdapterList() {
return ADAPTERS;
}

}

Die Klasse adaptiert also die Klasse Person, und liefert ein hierfür passenden PresentationModel zurück.

Der nächste Schritt besteht jetzt darin, sowohl die Widgets in der UI an die Observables aus dem PresentationModell als auch die Observables des PresentationModels an die Attribute des Models zu binden. Im Editor könnte das so aussehen:

private void initBindings() {
DataBindingContext context = new DataBindingContext();
context.bindValue(SWTObservables.observeText(emailText, SWT.FocusOut),
pm.getEmailTextValue(), null, null);

context.bindValue(SWTObservables.observeText(nameText, SWT.FocusOut),
pm.getNameValue(), null, null);

context.bindValue(ViewersObservables
.observeSingleSelection(titleViewer), pm.getTitleValue(), null,
null);

context.bindValue(SWTObservables.observeSelection(emailButton), pm
.isEmailButtonChecked(), null, null);

context.bindValue(SWTObservables.observeEnabled(emailText), pm
.isEmailFieldEnabled(), new UpdateValueStrategy(
UpdateValueStrategy.POLICY_NEVER), null);

context.bindValue(SWTObservables.observeEnabled(saveButton), pm
.isSaveButtonEnabled(), new UpdateValueStrategy(
UpdateValueStrategy.POLICY_NEVER), null);

}

Im PresentationModel könnte das Ganze so aussehen:

context.bindValue(emailFieldValue, BeansObservables.observeValue(
person, „email“), null, null);

context.bindValue(titleValue, BeansObservables.observeValue(person,
„title“), null, null);

context.bindValue(nameValue, BeansObservables.observeValue(person,
„name“), null, null);

Startet man das Ganze jetzt mit einer Demoperson, kann man erkennen, dass die Daten bereits an die Widgets gebunden sind. Bisher ist nichts gewonnen, definieren wir also die restlichen Bindings.

Schauen wir uns einige interessantere Bindings im Presentation-Model an:

context
.bindValue(emailFieldEnabledValue, emailButtonCheckedValue,
new UpdateValueStrategy(
UpdateValueStrategy.POLICY_NEVER), null);

Hier binden wir das emailFieldEnabledValue an den EmailButton, daraus ergibt sich, dass das Email-Feld nur enabled ist, wenn die Checkbox aktiviert ist.

Der Save Button soll nur Enabled sein, wenn eine gültige Email oder keine Email eingegeben wurde.

context.bindValue(saveButtonEnabledValue, emailFieldValue,
new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER),
new UpdateValueStrategy().setConverter(new Converter(
String.class, Boolean.class) {
@Override
public Object convert(Object fromObject) {
String str = fromObject.toString();
if („“.equals(str) || str
.matches(„[a-zA-Z0-9\\.]+@[a-zA-Z0-9]+\\.[a-z]{2,3}“)) {
return true;
}
return false;
}
}));

Wir verwenden hier einen Converter und eine Regexp zur Validierung der Email-Adresse.

Hier haben wir 2 Funktionlitäten, die es wert sind, getestet zu werden.

  1. Ist der SaveButton aktiviert, wenn keine Emailadresse eingegeben wurde?
  2. Ist der SaveButton aktiviert, wenn eine gültige Emailadresse eingegeben wurde?
  3. Ist der SaveButton deaktiviert, wenn eine ungültige Emailadresse eingegeben wurde?
  4. Ist das Emailfeld aktiviert, wenn die Checkbox aktiviert wurde?

Das schöne dabei ist, dass wir nur noch die Inhalte der entsprechenden Observables testen müssen, da der View (also unser Editor) im Prinzip

komplett dumm ist, und keine Logik enthält, sondern stattdessen seine Widgets nur gegen die Observables im PresentationModel bindet.

Definieren wir also ein Fragment, welches die Tests für unser Plugin definiert.

Die oben festgelegten Funktionalitäten lassen sich nun sehr einfach testen, ein Testfall könnte in etwa so aussehen:

public class PresentationModelTest {

private IPersonPresentationModel model;

private Person person;

@Before
public void setUp() {
person = new Person(null,null,null);

model = new PersonViewPresentationModel(person);

}

@After
public void tearDown() {
person = null;

model = null;
}

@Test
public void testSaveInitialEnabled(){
assertTrue((Boolean)model.isSaveButtonEnabled().getValue());
}

@Test
public void testSaveEnabledLegalEmail(){
model.isSaveButtonEnabled().setValue(false);
person.setEmail(„test@test.de“);
assertTrue((Boolean)model.isSaveButtonEnabled().getValue());
}
@Test
public void testSaveEnabledEmptyEmail(){
model.isSaveButtonEnabled().setValue(false);
person.setEmail(„“);
assertTrue((Boolean)model.isSaveButtonEnabled().getValue());
}
@Test
public void testSaveDisabledIllegalEmail(){
person.setEmail(„test@de“);
assertFalse((Boolean)model.isSaveButtonEnabled().getValue());
}
@Test
public void testEmailFieldEnabledCheckBox(){
model.isSaveButtonEnabled().setValue(false);
assertFalse((Boolean)model.isEmailButtonChecked().getValue());
assertFalse((Boolean)model.isEmailFieldEnabled().getValue());

model.isEmailButtonChecked().setValue(true);
assertTrue((Boolean)model.isEmailFieldEnabled().getValue());
}
}

Lässt man die Tests laufen, sieht man sofort:

unittests

Ah, grün!! wunderbar, bauen wir doch mal einen Bug ein, um das Ganze in Aktino zu sehen, nehmen wir an,

es schleicht sich ein Fehler im Validator ein, so dass dieser immer true liefert, egal was man für eine Emailadresse eingibt.

context.bindValue(saveButtonEnabledValue, emailFieldValue,
new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER),
new UpdateValueStrategy().setConverter(new Converter(
String.class, Boolean.class) {
@Override
public Object convert(Object fromObject) {
if(true)
return true;

String str = fromObject.toString();
if („“.equals(str) || str
.matches(„[a-zA-Z0-9\\.]+@[a-zA-Z0-9]+\\.[a-z]{2,3}“)) {
return true;
}
return false;
}
}));

Jetzt lassen wir die Tests nochmals laufen, und sehen wie erwartet:

unittests_fail

Dieser Artikel liefert mit Sicherheit keine Brandneuen Erkenntnisse, noch sind die Themen die er behandelet neu. Die Herangehensweise ist

aber dennoch interessant und düfrte für so manches Projekt von Belang sein. Ich hoffe, ich konnte euch ein wenig für das Thema begeistern, viel Spass damit, über Feedback würde ich mich freuen.

Den Source-Code zu der Demoanwendung findet man übrigens hier.

Advertisements

Ein Gedanke zu „RCP UI Testing mal ganz ohne Framework

  1. Simon Zambrovski

    Danke, netter post. Ich hab mir erlaubt etwas mehr von dem Blog anzuschauen. Was soll ich sagen – guter Inhalt, böse Darstellung. Poste leider als Kommentar, weil ich kein Impressum gefunden habe. Ein Paar Tipps: ein Template nehmen das breiter ist. Und einen Source Code Beautifier Plugin installieren, da man es sonst echt nicht lesen kann. Lust auf English zu posten? Ich koente mir vorstellen, dass der Inhalt bei TechJava auch gut ankommt. Dort haben wir im moment so um die 50 Leser pro Tag. Allerding geht dann das „heulen“ um die schlechte Eclipse Doku nicht mehr. Ich denke das zieht Ihren sehr guten Inhalt runter und erzählt nichts neues – das kennt jeder, und man kann es ja auch selbst verbessern.

    Antwort

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