My Maven multimodule project is built using JDK 21, and one of the modules is a tiny JavaFX app on OpenJFX 23.0.2.
I want to write a simple and fast unit test, that can be executed by my Gitlab CI runner during builds, checking for compatibility of the JavaFX dependency. So if someone were to run a CI pipeline with e.g. JavaFX 24, that requires JDK 22+, the unit test would fail. Since this unit test needs to be run in a headless CI, I really don't want to start the Toolkit, or the JavaFX Platform in the unit tests.
What are my options? I though I could simply try loading one of the fxml
files:
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getClassLoader().getResource("MainWindow.fxml"));
Parent root = fxmlLoader.load();
But even that seems excessive. I'd rather have something truly lightweight. I also tried creating a new Button:
Button button = new Button("Test");
Assertions.assertNotNull(button);
But that fails with java.lang.IllegalStateException: Toolkit not initialized
.
Is there a safe method call, or an object to instantiate in JavaFX, that would allow me to test the JDK compatibility in a truly headless mode?
Putting this sort of logic in the build system seems more appropriate to me than a unit test. For instance, you can use the Maven Enforcer Plugin. There are a couple rules that may fit your needs out-of-the-box.
requireJavaVersion (built-in rule) - make sure you're using the expected Java version (might not be effective if using toolchains).
requireSameVersion (built-in rule) - make sure all JavaFX modules have the same version.
enforceBytecodeVersion (third-party rule) - make sure none of the JavaFX dependencies were compiled for a newer version of Java.
You can write your own rule implementation if needed.
If you still want or need to implement this via a unit test then there are at least a couple options.
When JavaFX says it requires Java N+, that means the source code was compiled to target Java N. An UnsupportedClassVersionError
error is thrown when loading a class that targets Java N on Java M < N. Making use of this fact seems sufficient for your use case.
Any class from the javafx.base
module would do. That module is required by all other JavaFX modules, so it will always be present. And none of the classes in that module require the platform to be running.
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import javafx.util.Pair;
import org.junit.jupiter.api.Test;
class JfxCompatTest {
@Test
void testCanLoadJfx() {
assertDoesNotThrow(() -> new Pair<>(null, null));
}
}
If you want to get the version of JavaFX then the javafx.base
module has a javafx.properties
resource that you can load. Though I don't know if that resource is guaranteed to exist or what properties it's guaranteed to contain. At least in JavaFX 24 the resource contains:
javafx.version
(e.g., "24.0.1")
javafx.runtime.version
(e.g., "24.0.1+4")
javafx.runtime.build
(e.g., "4")
You can also try to get the javafx.version
value from the system properties. However, that system property is only set if the internal VersionInfo::setupSystemProperties()
method has been invoked. I'm not sure if there's any public API you can invoke to ensure the JavaFX system properties have been setup without at least initializing the toolkit.
You could of course access the VersionInfo
class directly or via reflection, but note that will require an appropriate --add-exports
argument if javafx.base
is a named module. It's also internal API so it may change between releases without notice.
Once you have the version then you can test to make sure it's not too recent.
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.util.Properties;
import org.junit.jupiter.api.Test;
class JfxCompatTest {
private static final int MAX_SUPPORTED_JFX_VERSION = 23;
static int getJfxFeatureVersion() throws IOException {
var props = new Properties();
try (var in = JfxCompatTest.class.getResourceAsStream("/javafx.properties")) {
props.load(in);
}
var version = props.getProperty("javafx.version");
return Runtime.Version.parse(version).feature();
}
@Test
void testJfxVersion() throws IOException {
int featureVersion = getJfxFeatureVersion();
if (featureVersion > MAX_SUPPORTED_JFX_VERSION) {
fail("Unsupported JavaFX version: " + featureVersion);
}
}
}
You would have to manually update the MAX_SUPPORTED_JFX_VERSION
value when you target a new Java version. Or you can keep a map of JavaFX versions to minimum Java versions and use Runtime::version()
to get the current Java version. Though you would still need to manually update the map as new versions of JavaFX are released (fail the test if an unknown version of JavaFX is on the class-path/module-path).