23 September 2018

As of Java 11, JavaFX is no longer packaged with the runtime but is a seperate module. Go to the OpenJFX website for "Getting Started" docs. In this post, I will provide a minimal setup for building and testing a OpenFX 11 application. The purpose is not to describe the steps in detail, but to have some Gradle- and code-samples at hand for myself.

Of course, you will need Java 11. As of this writing, Java 11 is not released so you will need to get an early-access version.

The Application-class looks like this:

package sample;

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

public class HelloFX extends Application {

	public static class Controller {

		@FXML
		TextField inputField;

		@FXML
		Label label;

		@FXML
		Button applyButton;

		public void applyButtonClicked() {
			label.setText(inputField.getText());
		}
	}

	@Override
	public void start(Stage stage) throws IOException {
		Parent root = FXMLLoader.load(getClass().getResource("/sample.fxml"));
		Scene scene = new Scene(root, 640, 480);
		stage.setScene(scene);
		stage.show();
	}

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

The controller is embeeded to simplify the example. It is used from within the sample.fxml under src/main/resources.

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>

<GridPane alignment="center" hgap="10" vgap="10" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.HelloFX$Controller">
   <children>
            <TextField id="input" fx:id="inputField" layoutX="15.0" layoutY="25.0" />
            <Label id="output" fx:id="label" layoutX="15.0" layoutY="84.0" text="TEXT GOES HERE" GridPane.rowIndex="1" />
            <Button id="action" fx:id="applyButton" layoutX="124.0" layoutY="160.0" mnemonicParsing="false" onAction="#applyButtonClicked" text="Apply" GridPane.rowIndex="2" />
   </children>
   <columnConstraints>
      <ColumnConstraints />
   </columnConstraints>
   <rowConstraints>
      <RowConstraints />
      <RowConstraints minHeight="10.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" prefHeight="30.0" />
   </rowConstraints>
</GridPane>

Of course, we want to write tested code. So, we can write a UI-test using TestFX.

package sample;

import java.io.IOException;

import org.junit.jupiter.api.Test;
import org.testfx.api.FxAssert;
import org.testfx.framework.junit5.ApplicationTest;
import org.testfx.matcher.control.LabeledMatchers;

import javafx.stage.Stage;

public class HelloFXTest extends ApplicationTest {

	@Override
	public void start(Stage stage) throws IOException {
		new HelloFX().start(stage);
	}

	@Test
	public void should_drag_file_into_trashcan() {
		// given:
		clickOn("#input");
		write("123");

		// when:
		clickOn("#action");

		// then:
		FxAssert.verifyThat("#output", LabeledMatchers.hasText("123"));
	}
}

Now, the build.gradle that ties it all together.

apply plugin: 'application'

def currentOS = org.gradle.internal.os.OperatingSystem.current()
def platform
if (currentOS.isWindows()) {
    platform = 'win'
} else if (currentOS.isLinux()) {
    platform = 'linux'
} else if (currentOS.isMacOsX()) {
    platform = 'mac'
}

repositories {
    mavenCentral()
}

dependencies {
    // we need to depend on the platform-specific libraries of openjfx
    compile "org.openjfx:javafx-base:11:${platform}"
    compile "org.openjfx:javafx-graphics:11:${platform}"
    compile "org.openjfx:javafx-controls:11:${platform}"
    compile "org.openjfx:javafx-fxml:11:${platform}"

    // junit 5
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'

    // testfx with junit5 binding
    testImplementation 'org.testfx:testfx-core:4.0.14-alpha'
    testImplementation 'org.testfx:testfx-junit5:4.0.14-alpha'
}

// add javafx modules to module-path during compile and runtime
compileJava {
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'javafx.controls,javafx.fxml'
        ]
    }
}

run {
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'javafx.controls,javafx.fxml'
        ]
    }
}

test {
    // use junit5 engine in gradle
    useJUnitPlatform()
    // log all tests
    testLogging {
        events 'PASSED', 'FAILED', 'SKIPPED'
    }
    // log output of tests; enable when needed
    //test.testLogging.showStandardStreams = true
}

mainClassName='sample.HelloFX'

Some comments are give as part of the code. So, no further explaination is give here.

Execute gradle test to run the tests. Execute gradle run to just run the application.