TDD in der GUI-Entwicklung

TestFX: JUnit-Tests für JavaFX

Sven Ruppert
© shutterstock.com/svilen_mitkov

TDD in der GUI-Entwicklung ist für viele Entwickler ein etwas unbeliebtes Thema. Wer kennt sie nicht, die Werkzeuge die einem die Arbeit abnehmen wollen? Aber aus Sicht eines Entwicklers sind diese meist weniger praktisch. Es werden Tools benötigt, mit denen schnell eine partielle Komponente getestet werden kann. Genau das verspricht das kleine Open-Source-Projekt TestFX. Wie funktioniert es? Wie wird es verwendet? Hält es was es verspricht?

TestFX ist ein kleines Open-Source-Projekt von SmartBear und erhebt den Anspruch, dem Entwickler schnell und unkompliziert zu helfen, jUnit-Tests für JavaFX-Elemente zu schreiben. Wir werden anhand eines kleinen Beispiels den Einsatz als auch die notwendigen Projektelemente betrachten. Beginnen wir mit der Projektinitialisierung.

Projektinitialisierung

In unserem Fall ist das recht einfach, da es sich lediglich um eine minimalistische pom.xml mit zwei Abhängigkeiten handelt. Bei Verwendung des JDK8 ist die pom.xml ein wenig kleiner, da JavaFX keine besondere Definition benötigt, wie es beim JDK7 notwendig ist. Die notwendigen Abhängigkeiten für das Hinzufügen von TestFX-Funktionalität sind in Listing 1 zu sehen. Sicherlich sind die Versionsnummern anzupassen. Nun steht der Verwendung von TestFX nichts mehr im Wege und es kann mit dem Schreiben von jUnit-Tests begonnen werden.

<dependency>
    <groupId>org.loadui</groupId>
    <artifactId>testFx</artifactId>
    <version>3.0.0 </version>
</dependency>
<dependency>
    <groupId>org.hamcrest

HelloWorld mit TestFX

Wie nun seit Jahrzehnten üblich beginnen auch wir hier mit dem Klassiker, einem HelloWorld. Der Grundaufbau eines jUnit-Test auf Basis von TestFX besteht aus einer Klasse abgeleitet von der abstrakten Klasse org.loadui.testfx.GUITest. Die zu implementierende Methode getRootNode() liefert das zu testende GUI-Element als Instanz der Klasse javafx.scene.Parent zurück. Die programmatisch erzeugte Instanz ist diejenige, auf der die nachfolgenden Tests ausgeführt werden. In unserem Beispiel ist es ein Button  (Listing 2).

@Category(TestFX.class)
public class SimpleButtonTest extends GuiTest {

    @Override
    protected Parent getRootNode() {
        final Button btn = new Button();
        btn.setId("btn");
        btn.setText("Hello World");
        btn.setOnAction((actionEvent)- > btn.setText( "was clicked" ));
        return btn;
    }
}

Was man hier sehr schön erkennen kann: die GUI-Elemente müssen in keine Hülle wie z.B. eine VBox eingebettet werden. Es kann tatsächlich auf einer beliebigen GUI-Komponente gearbeitet werden. Mit diesem Schritt ist die Basis geschaffen um die funktionalen Testmethoden zu schreiben.

Der erste Test

Wie bei jUnit üblich werden die Testmethoden mit der Annotation @Test annotiert um sie bei der Ausführung zu berücksichtigen. Nun stellt sich die Frage, wie auf die Instanz die durch die Methode getRootNode() erzeugt wird, zugegriffen werden kann. In unserem Fall die Instanz der Klasse Button. Hier liefert TestFX einige Servicemethoden. Die hier notwendige ist die Methode find(String query) aus der Klasse GUITest selbst. Der Name des Attributes aus der Methodensignatur lässt vermuten, dass hier mehr als nur eine ID übergeben werden kann. In diesem Fall ist es die Möglichkeit einen CSS-Selector zu verwenden, was wir in unserem Beispiel nicht verwenden, wir nehmen einfach direkt die ID der Instanz, um genau zu sein die FXID #btn (Listing 3).

@Test
public void shouldClickButton(){
    final Button button = find( "#btn" );
    click(button);
    verifyThat( "#btn", hasText("was clicked") );
}

Nachdem die Instanz erhalten wurde, kann mit den funktionalen Tests begonnen werden. Auch hier stellt TestFX einem Entwickler eine Menge Service-Methoden bereit. Allgemein gesagt, wird immer der Ansatz verfolgt alles mit dem Builder-Pattern abzubilden. Damit lassen sich die Ausdrücke, die eine Navigation und Verwendung der GUI Elemente beschreiben, sehr kompakt formulieren.

Und damit sind wir bei dem zweiten Grundgedanken von TestFX. Die Formulierung der Testfälle soll möglichst dem Verhalten bei der Verwendung nachempfunden werden. In unserem Beispiel das Klicken auf den Button, das durch die Methode click(button) realisiert wird. Ist die gewünschte Aktion erfolgt, wird überprüft ob das Ergebnis der Reaktion mit dem der Erwartung übereinstimmt. Hier soll durch das Drücken auf den Button der Text von „Hello World“ auf „was clicked“ verändert werden. Ob das stattgefunden hat, kann mittels der Methode verifyThat(fxid, matcher) geprüft werden. An dieser Stelle wird der Entwickler mit dem Framework Hamcrest konfrontiert, da die logischen Überprüfungen mittels Matcher realisiert worden sind. Beispiele befinden sich in dem Package org.loadui.testfx.matchers.

Aufmacherbild: Online trading concept von Shutterstock / Urheberrecht: svilen_mitkov [ header = Seite 2: Der Aufbau von TestFX ]

Der Aufbau von TestFX

Sehen wir uns nun die Funktionsweise und den Aufbau von TestFX an. Als Startpunkt nehmen wir die Klasse GuiTest, da sie der Einstiegspunkt für den Entwickler ist. Die Klasse GuiTest ist abstract und erfordert lediglich die Implementierung der Methode getRootNode() und bietet eine Menge Servicefunktionen. Die Methoden bilden maßgeblich das Grundgerüst zur Definition der Testfälle. Methoden sind unter anderem

  • find: zum Auffinden von bestimmten GUI Elementen
  • click: Simulieren eines Maus-Klicks auf einem GUI Element
  • rightClick: Simulieren eines Maus-Klicks mit der rechten Maustaste
  • doubleClick: Simulieren eines Doppelklicks
  • drag: Drag und Drop simulieren
  • move: Mausbewegung auf eine Position bzw ein GUI Element
  • press/release: Mausklick/Tastenklick mit zeitlicher Dauer
  • scroll: Rollen des Mausrades
  • push: drücken von Tasten

Damit stehen einem als Entwickler fast alle Interaktionsmöglichkeiten offen, um Benutzerverhalten zu simulieren. Innerhalb der Klasse GuiTest befindet sich die Klasse TestFxApp extends Application (Listing 4) und definiert den Einstiegspunkt um eine JavaFX-Applikation zu starten. Letzten Endes muss jeder jUnit-Test in einer eigenen oder alle Tests nacheinander in derselben Applikation laufen. Und hier beginnen die meisten Probleme. Denn schreibt man als Entwickler auf herkömmlichem Wege die JavaFX jUnit Tests, so landet man schnell bei der Tatsache, dass immer nur eine Instanz der Klasse Application zur selben Zeit innerhalb einer JVM aktiv sein darf.

public static class TestFxApp extends Application
{
    private static Scene scene = null;

    @Override
    public void start( Stage primaryStage ) throws Exception
    {
        primaryStage.initStyle(StageStyle.UNDECORATED);
        primaryStage.show();
        stageFuture.set( primaryStage );
    }

    public static void setRoot( Parent rootNode )
    {
        scene.setRoot( rootNode );
    }
}

Nun stellt sich die Frage, wie n verschiedene jUnit-Tests gestartet werden. Der Grundgedanke ist recht einfach. Jeder Test wird in einem eigenen Thread verpackt. Innerhalb des Runnable wird Application.launch(..) aufgerufen. Da jede jUnit-Test-Klasse von GuiTest abgeleitet worden ist, besteht immer eine 1-zu-1-Beziehung zwischen einer Instanz der jUnit-Testklasse und der JavaFX Application. Die Tests innerhalb einer Klasse laufen damit immer in einer einzelnen JavaFX-Instanz. Realisiert worden ist dies ab dem Methodenaufruf showNodeInStage in der Klasse GuiTest. Der Aufruf erfolgt getriggert durch die Annotation @Before vor der Ausführung der jUnit-Tests (Listing 5).

@Before
public void setupStage() throws Throwable{
    showNodeInStage();
}

private void showNodeInStage(){
    showNodeInStage(null);
}

private void showNodeInStage( final String stylesheet ){
    GuiTest.stylesheet = stylesheet;

    if( stage == null ){
        FXTestUtils.launchApp(TestFxApp.class);
        try{
            stage = targetWindow(stageFuture.get( 25,TimeUnit.SECONDS ) );
            FXTestUtils.bringToFront( stage );
        }catch( Exception e ) {
            throw new RuntimeException( "Unable to show stage", e );
        }
    }

    try{
        FXTestUtils.invokeAndWait( new Runnable(){
            @Override
            public void run(){
                Scene scene = SceneBuilder
                    .create()width( 600 ) height( 400 )
                    .root( getRootNode() ).build();

                if(stylesheet!=null) 
                    scene.getStylesheets().add(stylesheet);
                stage.setScene( scene );
            }
        }, 5 );
    }catch( Exception e ) {
        e.printStackTrace();
    }
}

Sollte es in der JVM noch keine Instanz der Klasse Stage im static Attribut stage der Klasse GuiTest geben, verpackt der Methodenaufruf FXTestUtils.launchApp(..) den Methodenaufruf Application.launch(..) in ein Runnable() und startet den Thread. Das passiert demnach nur einmal pro JVM. Alle nachfolgenden Aufrufe werden sofort an FXTestUtils.invokeAndWait(..) weitergeleitet. Das Ergebnis ist eine Liste von Callable-Instanzen (eine pro Test) die der Reihe nach abgearbeitet werden. Zu beachten ist hier, dass die Abarbeitung nicht zu lange dauern sollte, da der Aufruf mit einem Timeout versehen worden ist.

Fazit

Das Framework TestFX ist für den Entwickler eine echte Erleichterung. Die benötigten Zusatzbibliotheken sind sehr wenige. Das Framework selbst ist sehr klein, was es einem Entwickler erlaubt die Funktionsweise sehr schnell auch im Detail zu verstehen. Die Integration in eine CI-Strecke ist dank der Einbettung in jUnit sehr einfach. Sollte mal ein Test fehlschlagen, wird von der Situation ein Screenshot gemacht und auf der Festplatte gespeichert.

Im nächsten Artikel über TestFX werden wir über die derzeitige Integration von CDI in dieses Framework berichten. Ich kann schon so viel verraten, es wird spannend werden!

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]. TestFX selbst finden Sie unter [3].

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

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: