javamavenunit-testingjavafxopenjfx

How to test for JDK - OpenJFX compatibility in a headless unit test?


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?


Solution

  • Build System

    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.

    You can write your own rule implementation if needed.


    Unit Test

    If you still want or need to implement this via a unit test then there are at least a couple options.

    By Class File Version

    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));
      }
    }
    

    By JavaFX Version

    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:

    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).