Funktion und Unterschied zu TestFX

JavaFX testen mit JDK8: MarvinFX

Sven Ruppert
©Shutterstock.com/svilen_mitkov

JavaFX-Anwendungen zu testen ist immer wieder eine Herausforderung. Eine Alternative zu TestFX ist das Framework von Hendrik Ebbers aus Dortmund. Wie funktioniert es? Wie ist es aufgebaut? Was ist der Unterschied zu TestFX?

Mit MarvinFX kommt ein weiteres Test-Framework für JavaFX in den Ring. Wir werden anhand von Beispielen den Funktionsumfang beleuchten und auf einzelne Unterschiede aufmerksam machen.

Hello MarvinFX – World

MarvinFX erhebt den Anspruch ein einfach zu verwendendes Test-Framework für JavaFX-Controls und -Scenes zu sein. Der Schwerpunkt soll hier auf dem Umgang mit Properties liegen. Es ist allerdings zu beachten, dass dieses Framework sich noch in einem sehr frühen Stadium befindet. Der Grundgedanke ist recht einfach: Der Entwickler soll die benötigten Controls instanziieren, anzeigen und dann die Interaktion des Benutzers simulieren. Die Zwischenstände werden mittels Prüfmethoden validiert. Sehen wir uns demnach ein sehr einfaches Beispiel in Listing 1 an.

@Test
public void test1() {
    Button b1 = new Button("Test123");
    MarvinFx.show(b1);
}

Dieses Beispiel erzeugt einen Button und zeigt diesen auf dem Bildschirm an. Leider ist das nur die halbe Miete. Es fehlt hier noch ein wichtiger Schritt: die Initialisierung der JavaFX-Umgebung. Das holen wir einfach nach, indem wir in der @Before-Methode eine Instanz der Klasse JFXPanel erzeugen. Das ist sicherlich nicht der eleganteste Weg, jedoch an diese Stelle der schnellste. Bei TestFX wurde dieses durch die Ableitung von der Klasse GuiTest gelöst. Hier in diesem Beispiel (Listing 1) hat noch kein einziger Test stattgefunden.

Der erste logische Test

Erweitern wir den Test aus Listing 1 nun um eine Überprüfung (Listing 2).

@Test
public void test2() {
    Button b1 = new Button("Test123");
    MarvinFx.show(b1);
    PropertySupervisor<String> textPropertySupervisor 
        = new PropertySupervisor<>(b1.textProperty());
    textPropertySupervisor.assertValueIsEquals("Test123");
}

Hier kommen wir mit dem PropertySupervisor in Kontakt. Seine Aufgabe ist es, Veränderungen zu detektieren und dann einen definierten Satz an Rules anzuwenden. Ein PropertySupervisor hat Servicemethoden ähnlich denen von Asserts aus dem JUnit-Framework. In Listing 2 wird die Methode assertValueIsEqual(..) verwendet. Diese Art und Weise sollte einem von JUnit her bekannt vorkommen. Wichtig an dieser Stelle ist, dass ein bestimmtes Property dem Supervisor übergeben wird. Dieses Property muss selbst von ObservableValue ableiten. Dadurch wird sichergestellt, dass dem Attribut Listener hinzugefügt werden können. Und der erste Listener den ein PropertySupervisor dem Attribut hinzufügt, ist er selbst.
Kommen wir zu den Asserts. Hier werden durch den Aufruf der Methoden die mit dem Prefix assert beginnen Rules instanziiert und dann entweder der Methode checkAssertion(..) direkt übergeben oder mittels der Methode addFutureRule(..) in die Liste der aktiven Rules eingefügt. Die Methoden assertValueIsEquals, assertValueIsNull und assertValueIsNotNull werden sofort ausgewertet. Alle anderen assertXXX Methoden werden erst ausgewertet, wenn die Methode confirm() des jeweiligen Supervisors aufgerufen wird. Damit hat man nun unterschiedliches Verhalten. Das eine wird sofort ausgewertet, das andere kann erst aufgebaut werden um es dann zu einem spezifischen Zeitpunkt auszulösen. Warum dieser Unterschied?

Aufmacherbild: Online trading concept von Shutterstock / Urheberrecht: svilen_mitkov

[ header = Seite 2: Logische Sequenzen ]

Logische Sequenzen

Unter logischen Sequenzen verstehen wir in diesem Kontext den Ablauf den ein Property vom Wert her durchläuft basierend auf den Benutzer- oder Programminteraktionen. Das soll bedeuten, dass z.B. durch jeden Mouse-Click auf den Button sich die Beschriftung in einer fest vorgegebenen Sequenz ändert. Um das Beispiel einfach zu halten, werden wir ein Textfeld nehmen und den Wert programmatisch in der Reihenfolge „1“ , “7“ ,“14“ wechseln (Listing 3).

@Test
public void test3() {
    TextField textField = new TextField("1");
    MarvinFx.show(textField);
    
    textfieldFixture.setText("7");
    textfieldFixture.setText("14");
}

Nun stellt die Frage, wie der Ablauf verifiziert werden kann. Hierzu verwenden wir eine Instanz der Klasse TextfieldFixture und übergeben die Instanz des Textfeldes. Der TextSupervisor kann entweder über die Service-Methode des TextFieldFixture erzeugt oder wie gewohnt selbst instanziiert werden. Es besteht kein Unterschied, da in der Servicemethode lediglich der Konstruktor des korrespondierenden Supervisors verwendet wird (Listing 3a).

@Test
public void test3() {
    TextField textField = new TextField("1");
    MarvinFx.show(textField);
    TextfieldFixture textfieldFixture = new TextfieldFixture(textField);
    PropertySupervisor<String> textSupervisor =
        textfieldFixture.createTextPropertySupervisor();

    textfieldFixture.setText("7");
    textfieldFixture.setText("14");
}

Nun muss noch der Testablauf selbst definiert werden. Das wird bei MarvinFX mit Hilfe der assertWillXXX(..) Methoden realisiert. Mittels assertWillChange() wird eine Änderung angemeldet und mit assertWillChangeByDefinedCount(x) legt der Entwickler fest, wie viele Änderungen stattfinden werden. In unserem Beispiel werden basierend auf dem Initialwert zwei Änderungen durchgeführt. Der Wechsel von 1 auf 7 und der Wechsel von 7 auf 14 (Listing 3b).

@Test
public void test3() {
    TextField textField = new TextField("1");
    MarvinFx.show(textField);
    TextfieldFixture textfieldFixture = new TextfieldFixture(textField);
    PropertySupervisor<String> textSupervisor =
        textfieldFixture.createTextPropertySupervisor();

    textSupervisor.assertWillChange();
    textSupervisor.assertWillChangeByDefinedCount(2);
    textSupervisor.assertWillChangeThisWay("7", "14");
    textfieldFixture.setText("7");
    textfieldFixture.setText("14");
}

Sind alle Änderungen und Zustände definiert, wird mittels confirm() dieses überprüft (Listing 3c).

@Test
public void test3() {
    TextField textField = new TextField("1");
    MarvinFx.show(textField);
    TextfieldFixture textfieldFixture = new TextfieldFixture(textField);
    PropertySupervisor<String> textSupervisor =
        textfieldFixture.createTextPropertySupervisor();

    textSupervisor.assertWillChange();
    textSupervisor.assertWillChangeByDefinedCount(2);
    textSupervisor.assertWillChangeThisWay("7", "14");
    textfieldFixture.setText("7");
    textfieldFixture.setText("14");
    textSupervisor.confirm();
}

Sollte es hierbei einen Fehler geben, wird der Test abgebrochen und als fehlerhaft markiert.

Auslösen von Benutzerinteraktionen

Nun fehlt noch die Simulation der Benutzerinteraktion, z.B. mittels Maus. Dafür hält der entsprechende Supervisor die Methode mouse() bereit. Hiermit bekommt man die Instanz mit der die Mausinteraktionen dargestellt werden. Die Methode click(..) führt einen bis n Klicks aus. Dadurch, dass immer der passende Supervisor verwendet wird, ist ein Klick auch eindeutig einer Komponente zugeordnet.

[ header = Seite 3: Komplexeres Beispiel ]

Komplexeres Beispiel

Aus diesen Grundkomponenten kann man nun recht komplexe Muster aufbauen. Beispielhaft soll hier das Listing 4 dienen.

@Test
public void test4() {
    final TextField textField = new TextField("1");
    Button button = new Button("Button");
    button.setOnAction(new EventHandler<ActionEvent>() {

        @Override
        public void handle(ActionEvent event) {
            int value = Integer.parseInt(textField.getText()) * 2;
            textField.setText(value + "");
        }
    });
    MarvinFx.show(VBoxBuilder.create()
        .children(textField, button).build());

    TextfieldFixture textfieldFixture 
        = new TextfieldFixture(textField);
    PropertySupervisor<String> textSupervisor 
        = textfieldFixture.createTextPropertySupervisor();
    NodeFixture<Button> buttonFixture 
        = new NodeFixture<Button>(button);

    textSupervisor.assertValueIsNotNull();
    textSupervisor.assertValueIsEquals("1");
    buttonFixture.mouse().click();
    textSupervisor.assertValueIsEquals("2");

    textSupervisor.assertWillChange();
    textSupervisor.assertWillChangeByDefinedCount(4);
    textSupervisor.assertWillChangeThisWay("4", "8", "16", "32");
    buttonFixture.mouse().click(4);
    textSupervisor.confirm();

    textSupervisor.assertWillNeverChange();
    textSupervisor.confirm();

    textSupervisor.assertWillChange();
    textSupervisor.assertWillChangeByDefinedCount(2);
    textSupervisor.assertWillChangeThisWay("7", "14");
    textfieldFixture.setText("7");
    buttonFixture.mouse().click();
    textSupervisor.confirm();
}

Fazit

Obwohl Hendrik Ebbers sein Projekt noch als sehr jung und nicht stabil bezeichnet, kann man doch schon recht viele Fälle damit abdecken. Im Einsatz haben diese Funktionen gut funktioniert und es konnte eine aussagekräftige Testabdeckung erzielt werden. Besonders hervorzuheben ist der Ansatz, einen eigenen Robot zu verwenden. Das ist in TestFX im Moment nicht der Fall, was wiederum den Einsatz von TestFX nur auf Plattformen zulässt die den AWT-Robot bereitstellen. Auf der anderen Seite ist TestFX stärker wenn es um die Darstellung von Benutzerinteraktionen mittels Drag&Drop geht.
In einem weiteren Artikel werden wir uns zum Einen den Robot von MarvinFX ansehen und zum anderen einen CDI-Support einbauen.
Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "JavaFX testen mit JDK8: MarvinFX"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Hendrik Ebbers
Gast

Weitere Informationen zu MarvinFX gibt es hier:

http://www.guigarage.com/2013/03/introducing-marvinfx/
http://www.guigarage.com/2013/03/assertions-and-rules-in-marvinfx/

Wie von Sven bereits geschrieben ist das ganze momentan noch in einem sehr frühen Zustand. Es ist alleridngs geplant, das Framework in diesem Jahr weiter zu entwickeln.