01.12.2015

JavaFX und Spring verbinden und Testen

Produktivcode

Seit der Version 8 der Programmiersprache Java ist JavaFX für Desktop Anwendungen fester Bestandteil. Somit ist es möglich, Platform übergreifend mit einmaligen kompilieren der Anwendung das neue Framework einzusetzen.  Spring hingegen ist ein seit langen bewertes Framework unteranderem für Dependency Injection.

Im folgenden wollen wir zeigen, wie man eine JavaFX Programm mit mehreren Views mit Dependency Injection verbinden.

Zuerst schauen wir uns einmal an, was wir alles benötigen. Wir erstellen die Views mit den JavaSceneBuilder und erhalten somit eine FXML. Zudem benötigen folgende folgende Frameworks. Diese wollen wir mit gradle runter laden.

dependencies {
    compile 'org.springframework:spring-context:4.2.2.RELEASE'    

    testCompile group: 'junit', name: 'junit', version: '4.11'
    testCompile 'org.mockito:mockito-core:1.10.8'
    testCompile 'de.saxsys:jfx-testrunner:1.0'
}

Die erste Klasse, die wir uns anschauen wollen ist der SpringContext. Da wir nur mit Annotationen arbeiten wollen, müssen wir einen entsprechenden Context erstellen. Dieser könnte so aussehen:

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(value={"your.package.structur"})
public class MySpringContext {
}

Um entsprechend mit unseren FXML Dateien interagieren zu können, müssen wir Controller erstellen. Jeder unserer Controller muss von einer speziellen Oberklasse abgeleitet werden. Diese sieht wie folgt aus (Quelle: http://codelife.de/2015/02/27/javafx-8-with-spring-integration/#highlighter_46028):

import org.springframework.beans.factory.InitializingBean;
import java.io.IOException;
import java.io.InputStream;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;

public abstract class FXMLController implements InitializingBean, Initializable {
    protected Parent view;
    protected String fxmlFilePath;
    @Autowired protected ChangeViewService changeViewService;

    public FXMLController() {
        super();
    }

    public abstract void setFxmlFilePath(String filePath);
    public abstract void initialize();

    @Override
    public void afterPropertiesSet() throws Exception {
        loadFXML();
    }

    public Parent getView() {
        return view;
    }

    protected final void loadFXML() throws IOException {
        try (InputStream fxmlStream = getClass().getResourceAsStream(fxmlFilePath)) {
            FXMLLoader loader = new FXMLLoader();
            loader.setController(this);
            this.view = (loader.load(fxmlStream));
        }
    }

}

Dieser ist zusätzlich mit einen Service ausgestattet, welcher dem Wechsel von den Views erledigt (s. unten).

Als nächstes wollen wir einen Beispielhaften Controller erstellen. Dieser kann nichts besonderes und soll verdeutlichen, was an so einen Controller geschrieben werden muss und was man damit machen kann.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;

@Controller
public class MyViewController extends FXMLController {
    @FXML private Button myButton;
    @FXML private CheckBox myCheckBox;

    @Autowired private OtherViewController otherViewController;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // Do something
    }

    @Override
    @Value("/path/in/resource/folder/MyView.fxml")
    public void setFxmlFilePath(String filePath) {
        fxmlFilePath = filePath;
    }

    @FXML
    private void handleMyButtonAction(ActionEvent event) throws IOException {
        changeViewService.changeView(view, otherViewController.getView());
    }
}

Nun kümmern wir uns, um das Wechseln der Views mit einen eigenen Service. Der Serive sieht wie folgt aus:

import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.stereotype.Component;

@Component
public class ChangeViewService {

    public void changeView(Parent currentView, Parent targetView) {
        Stage stage = (Stage) currentView.getScene().getWindow();
        Scene scene = new Scene(targetView, 1024, 768);
        stage.setScene(scene);
        stage.show();
    }

}

Zum Schluss des Produktivcodes wollen wir noch die Starterklasse schreiben. Diese könnte so aussehen:

import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class MyJavaFXApp extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        ApplicationContext springContext = new AnnotationConfigApplicationContext(MySpringContext.class);
        Scene scene = new Scene(springContext.getBean(MyViewController.class).getView());
        scene.getStylesheets().add( getClass().getResource("/path/to/application.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setHeight(768);
        primaryStage.setWidth(1024);
        primaryStage.show();
    }
}

Testen

Da JavaFX immer in einen entsprechenden JavaFX Thread laufen muss, müssen wir unsere Tests entsprechend vorbereiten. Wir setzten dazu den jfx-testrunner ein. Eine genauere Erklärung davon findet ihr hier: http://blog.buildpath.de/javafx-testrunner/.

Des weiteren wollen wir alles, was wir nicht Testen wollen (andere Controller, Services und ähnliches) mit Mockito mocken. So könnte der einfache Testaufbau aussehen.

import de.saxsys.javafx.test.JfxRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(JfxRunner.class)
public class MyViewControllerTest {

    @Mock private ChangeViewService changeViewService;
    @InjectMocks private MyViewController myViewController = new MyViewController();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shouldDoSomething() {

    }

}

Folgendes Problem haben wir nun: Sowohl unser Handler, als auch die JavaFX Elemente (Buttons, CheckBoxen und ähnliches) sind private und es gibt keine getter und setter. Wir müssen uns also nun eine Möglichkeit schaffen, diese Dinge zu injecten. Dazu schreiben wir uns eine Oberklasse für Controller-Tests, welche die Funktionalität zum Injecten von Elementen, sowie das ausführen von AchtionHandlern hat. Dieser TestRunner sieht so aus:

import javafx.event.ActionEvent;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public abstract class JavaFXControllerTestRunner {

    public void callPrivateHandleMethod(String methodName, Object controller, ActionEvent actionEvent) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method buttonHandlerMethod = controller.getClass().getDeclaredMethod(methodName, ActionEvent.class);
        buttonHandlerMethod.setAccessible(true);
        buttonHandlerMethod.invoke(controller, actionEvent);
    }

    public void injectJavaFXObjectToController(String fieldName, Object controller, Object javaFxObject) throws NoSuchFieldException, IllegalAccessException {
        Field javaFxElementField = controller.getClass().getDeclaredField(fieldName);
        javaFxElementField.setAccessible(true);
        javaFxElementField.set(controller, javaFxObject);
    }

}

Die erste Methode ruft hierbei einen EventHandler auf. Die zweite Methode injected dann ein Objekt in den Controller.

Wenn wir nun unseren Test mit den TestRunner erweitern kann man die Methoden wie folgt anwenden.

import de.saxsys.javafx.test.JfxRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(JfxRunner.class)
public class MyViewControllerTest extends JavaFXControllerTestRunner {

    @Mock private ChangeViewService changeViewService;
    @Mock private ActionEvent actionEvent;
    @InjectMocks private MyViewController myViewController = new MyViewController();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shouldDoSomething() throws Exception{
         CheckBox myCheckbox = new Checkbox();
         injectJavaFXObjectToController("theCheckBoxFieldName", MyViewController, myCheckbox);
         callPrivateHandleMethod("theHandlerMethodName", MyViewController, actionEvent);
         assertTrue(myCheckbox.isSelected());
    }

}

Abschließend ist noch zu erwähnen, dass es beim Automatisierten Testen mit einen CI Server zu Problemen kommen kann, sofern diese über kein GUI Framework besitzen. Auf Debain basierten Servern kann xvfb helfen.

Kategorien: Benutzeroberfläche, Entwicklung | 1 Kommentar